diff --git a/js/core.js b/js/core.js
new file mode 100644
index 0000000..e052e16
--- /dev/null
+++ b/js/core.js
@@ -0,0 +1,2276 @@
+// MIT License
+//
+// Copyright (c) 2016-2018, Alexander I. Chebykin
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+/**
+ * CAI CP v.0.9.1
+ *
+ * @module : Core
+ * @author : Alexander I. Chebykin
+ * @copyright : Copyright (c) 2016-2018 Alexander I. Chebykin
+ * @version : 0.9.1
+ * @build date : 2018-06-05
+ * @license : MIT
+ * @link : https://github.com/CAI79/CAI-CP
+ ******************************************************************/
+
+(function () {
+ "use strict";
+
+ /**
+ * Return GET parameter
+ *
+ * @param {string} key Key name
+ *
+ * returns {string}
+ */
+ function $_GET(key) {
+ var s = window.location.search;
+
+ s = s.match(new RegExp(key + "=([^&=]+)"));
+
+ return s ? s[1] : false;
+ }
+
+ /**
+ * Check value is numeric
+ *
+ * @param {variant} value Value to be checked
+ *
+ * @returns {Boolean}
+ */
+ function isNumeric(value) {
+ return !isNaN(parseFloat(value)) && isFinite(value);
+ }
+
+ /**
+ * Return string with formatted numeric value. ex: 12345,6 -> 12'345,6
+ *
+ * @param {variant} value Source value
+ * @param {separator} separator Character used to separate thousands
+ *
+ * @returns {String}
+ */
+ function numFormatted(value, separator) {
+ var result = "",
+ splitted = value.toString().split(" ");
+ for (var i = 0; i < splitted.length; i++) {
+ if (isNumeric(splitted[i])) {
+ result += " " +
+ splitted[i].toString().replace(/\B(?=(\d{3})+(?!\d))/g,
+ separator);
+ } else {
+ result += " " + splitted[i].toString();
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Add message with timestamp to browser's console
+ *
+ * @param {string} log_message Message text
+ *
+ * @returns {undefined}
+ */
+ function console_log(log_message) {
+ var now = new Date();
+ console.log(now + ": " + log_message);
+ }
+
+ /**
+ * Application constructor
+ *
+ * @returns {undefined}
+ */
+ function Application() {
+ /* Object types */
+ this.OT_TABLE_H = 1;
+ this.OT_TABLE_V = 2;
+ this.OT_IFRAME = 3;
+ this.OT_CANVAS = 4;
+ // this.OT_DIV = 5;
+
+ this.timers = [];
+ this.apps_list = {};
+ this.lang = {};
+ this.menu = {};
+ this.settings = {};
+ this.widgets = {};
+
+ this.blocks_rotation = ["system",
+ "system_pulse",
+ "storage",
+ "network",
+ "smb"];
+ this.cur_block = 0;
+
+ /* Monitoring variables */
+ this.mon_users = 0;
+ this.mon_users_info = {
+ "count" : 0,
+ "last_user": "",
+ "last_addr": ""
+ };
+
+ /* Ctrl pressed flag */
+ this.ctrl_on = false;
+
+ /* Down point for swipe event on header */
+ this.hdr_x_down = null;
+ this.hdr_y_down = null;
+
+ this.init(1);
+ }
+
+ /**
+ * Add events handlers
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.add_evt_handlers = function () {
+ var app_instance = this;
+
+ /* Add swipe handler to header */
+ document.getElementById("app_header").addEventListener("touchstart",
+ function (event) {
+ app_instance.hdr_x_down = event.touches[0].clientX;
+ app_instance.hdr_y_down = event.touches[0].clientY;
+ },
+ false);
+ document.getElementById("app_header").addEventListener("touchmove",
+ function (event) {
+ if (!app_instance.hdr_x_down || !app_instance.hdr_y_down) {
+ return;
+ }
+ var x_up = event.touches[0].clientX,
+ y_up = event.touches[0].clientY,
+ delta_x = app_instance.hdr_x_down - x_up,
+ delta_y = app_instance.hdr_y_down - y_up;
+
+ if (Math.abs(delta_x) > Math.abs(delta_y)) {
+ if (delta_x > 0) {
+ /* left swipe */
+ app_instance.cur_block += 1;
+ if (app_instance.cur_block > app_instance.blocks_rotation.length - 1) {
+ app_instance.cur_block = 0;
+ }
+ } else {
+ /* right swipe */
+ app_instance.cur_block -= 1;
+ if (app_instance.cur_block < 0) {
+ app_instance.cur_block = app_instance.blocks_rotation.length - 1;
+ }
+ }
+ app_instance.widgets_run(app_instance.blocks_rotation[app_instance.cur_block]);
+ if (app_instance.blocks_rotation[app_instance.cur_block] === "system") {
+ app_instance.os_logo_draw();
+ }
+ // } else {
+ // if (delta_y > 0) {
+ /* up swipe */
+ // } else {
+ /* down swipe */
+ // }
+ }
+ /* reset values */
+ app_instance.hdr_x_down = null;
+ app_instance.hdr_y_down = null;
+ },
+ false);
+
+ /* Add ctrl pressed flag switcher */
+ document.addEventListener("keydown", function (event) {
+ if ((event.which === 17) && !app_instance.ctrl_on) {
+ app_instance.ctrl_on = true;
+ }
+ if (app_instance.ctrl_on) {
+ /* Temporary turn off ctrl flag */
+ app_instance.ctrl_on = false;
+ switch (event.which) {
+ case 37: // Ctrl + <-
+ app_instance.cur_block -= 1;
+ if (app_instance.cur_block < 0) {
+ app_instance.cur_block = app_instance.blocks_rotation.length - 1;
+ }
+ app_instance.widgets_run(app_instance.blocks_rotation[app_instance.cur_block]);
+ if (app_instance.blocks_rotation[app_instance.cur_block] === "system") {
+ app_instance.os_logo_draw();
+ }
+ event.preventDefault();
+ break;
+ case 39: // Ctrl + ->
+ app_instance.cur_block += 1;
+ if (app_instance.cur_block > app_instance.blocks_rotation.length - 1) {
+ app_instance.cur_block = 0;
+ }
+ app_instance.widgets_run(app_instance.blocks_rotation[app_instance.cur_block]);
+ if (app_instance.blocks_rotation[app_instance.cur_block] === "system") {
+ app_instance.os_logo_draw();
+ }
+ event.preventDefault();
+ break;
+ case 49: // Ctrl + 1
+ app_instance.widgets_run("system");
+ app_instance.os_logo_draw();
+ event.preventDefault();
+ break;
+ case 50: // Ctrl + 2
+ app_instance.widgets_run("system_pulse");
+ event.preventDefault();
+ break;
+ case 51: // Ctrl + 3
+ app_instance.widgets_run("storage");
+ event.preventDefault();
+ break;
+ case 52: // Ctrl + 4
+ app_instance.widgets_run("network");
+ event.preventDefault();
+ break;
+ case 53: // Ctrl + 5
+ app_instance.widgets_run("smb");
+ event.preventDefault();
+ break;
+ case 112: // Ctrl + F1
+ app_instance.help_show(false);
+ event.preventDefault();
+ break;
+ }
+
+ app_instance.ctrl_on = true;
+ }
+ });
+ document.addEventListener("keyup", function (event) {
+ if ((event.which === 17) && app_instance.ctrl_on) {
+ app_instance.ctrl_on = false;
+ }
+ });
+ };
+
+ /**
+ * Get data by uri
+ *
+ * @param {string} uri Data uri
+ *
+ * returns {undefined}
+ */
+ Application.prototype.get_data = function (uri) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function() {
+ resolve(this.responseText);
+ };
+ xhr.onerror = reject;
+ xhr.open("GET", uri);
+ xhr.send();
+ });
+ };
+
+ /**
+ * Initialize application
+ *
+ * @param {numeric} init_level Initialization level:
+ * 1 - Load settings,
+ * 2 - Load localization
+ * 3 - Initializa interface
+ *
+ * @returns {Boolean}
+ */
+ Application.prototype.init = function (init_level) {
+ var app_instance = this,
+ json_file;
+
+ switch (init_level) {
+ case 1:
+ /* Add events handlers */
+ this.add_evt_handlers();
+
+ /* Load user defined settings */
+ this.get_data("system/json/settings.json")
+ .then(function(result) {
+ app_instance.settings = JSON.parse(result);
+
+ app_instance.init(2);
+ })
+ .catch(function(result) {
+ console_log("init 1 error: " + result);
+ });
+
+ break;
+ case 2:
+ /* Load general settings */
+ this.get_data("system/json/general_settings/")
+ .then(function(result) {
+ var general_settings = JSON.parse(result);
+
+ app_instance.settings.shutdown_enabled =
+ general_settings.shutdown_enabled;
+ app_instance.settings.reboot_enabled =
+ general_settings.reboot_enabled;
+ app_instance.settings.w_transmission_enabled =
+ general_settings.w_transmission_enabled;
+
+ app_instance.init(3);
+ })
+ .catch(function(result) {
+ console_log("init 2 error: " + result);
+ });
+
+ break;
+ case 3:
+ /* Load locale */
+ if ($_GET("lang")) {
+ json_file = "system/json/locale/" + $_GET("lang") + ".json";
+ } else {
+ json_file = "system/json/locale/" + this.settings.lang + ".json";
+ window.history.pushState(null, null, "?lang=" + this.settings.lang);
+ }
+
+ this.get_data(json_file)
+ .then(function(result) {
+ app_instance.lang = JSON.parse(result);
+
+ app_instance.init(4);
+ })
+ .catch(function(result) {
+ console_log("init 3 error: " + result);
+ });
+
+ break;
+ case 4:
+ /* Files rights check */
+ if (this.settings.check_files_rights) {
+ this.get_data("system/scripts/check_files.php")
+ .then(function(result) {
+ if (JSON.parse(result).length > 0) {
+ var div_info = document.createElement("div"),
+ text_info = document.createTextNode(app_instance.lang.fs_errors);
+
+ div_info.style.cursor = "pointer";
+ div_info.appendChild(text_info);
+ div_info.addEventListener("click", function () {
+ app_instance.page_clear();
+ app_instance.widget_run(app_instance.w_table_files_check);
+ });
+ document.getElementById("div_attention_content").appendChild(div_info);
+ document.getElementById("div_attention").classList.remove("hidden");
+ }
+
+ app_instance.init(5);
+ })
+ .catch(function(result) {
+ console_log("init 4 error: " + result);
+ });
+ } else {
+ this.init(5);
+ return;
+ }
+ break;
+ case 5:
+ /* Interface init */
+ /* Menu elements */
+ this.menu = {
+ "div_menu_info": {
+ "id" : "div_menu_info",
+ "caption" : this.lang.information,
+ "visible" : true,
+ "separator" : false,
+ "elements": {
+ "info_system": {
+ "icon" : "gfx/icons/server.png",
+ "caption" : this.lang.system,
+ "callback" : function () {
+ app_instance.widgets_run("system");
+ app_instance.os_logo_draw();
+ }
+ },
+ "info_pulse": {
+ "icon" : "gfx/icons/sysinfo.png",
+ "caption" : this.lang.system_pulse,
+ "callback" : function () {
+ app_instance.widgets_run("system_pulse");
+ }
+ },
+ "info_storage": {
+ "icon" : "gfx/icons/storage.png",
+ "caption" : this.lang.storage,
+ "callback" : function () {
+ app_instance.widgets_run("storage");
+ }
+ },
+ "info_network": {
+ "icon" : "gfx/icons/network.png",
+ "caption" : this.lang.network,
+ "callback" : function () {
+ app_instance.widgets_run("network");
+ }
+ },
+ "info_smb": {
+ "icon" : "gfx/icons/samba.png",
+ "caption" : this.lang.smb,
+ "callback" : function () {
+ app_instance.widgets_run("smb");
+ }
+ }
+ }
+ },
+ "div_menu_app": {
+ "id" : "div_menu_app",
+ "caption" : this.lang.applications,
+ "visible" : false,
+ "separator" : true,
+ "elements": {}
+ },
+ "div_settings": {
+ "id" : "div_menu_settings",
+ "caption" : "",
+ "visible" : true,
+ "separator" : true,
+ "elements" : {
+ "cp_settings": {
+ "icon" : "gfx/icons/settings.png",
+ "caption" : this.lang.settings,
+ "callback" : function () {
+ app_instance.settings_show();
+ }
+ },
+ "cp_help_about": {
+ "icon" : "gfx/icons/about.png",
+ "caption" : this.lang.about,
+ "callback" : function () {
+ app_instance.help_show(true);
+ }
+ },
+ "cp_help": {
+ "icon" : "gfx/icons/help.png",
+ "caption" : this.lang.help,
+ "callback" : function () {
+ app_instance.help_show(false);
+ }
+ }
+ }
+ },
+ "div_shutdown": {
+ "id" : "div_menu_shutdown",
+ "caption" : "",
+ "visible" : true,
+ "separator" : true,
+ "elements": {
+ "srv_reboot": {
+ "icon" : "gfx/icons/reboot.png",
+ "caption" : this.lang.reboot,
+ "callback" : function () {
+ app_instance.shutdown(true);
+ }
+ },
+ "srv_shutdown": {
+ "icon" : "gfx/icons/shutdown.png",
+ "caption" : this.lang.shutdown,
+ "callback" : function () {
+ app_instance.shutdown(false);
+ }
+ }
+ }
+ }
+ };
+ if (!this.settings.shutdown_enabled && !this.settings.reboot_enabled) {
+ delete this.menu.div_shutdown;
+ } else {
+ if (!this.settings.shutdown_enabled) {
+ delete this.menu.div_shutdown.elements.srv_shutdown;
+ }
+ if (!this.settings.reboot_enabled) {
+ delete this.menu.div_shutdown.elements.srv_reboot;
+ }
+ }
+
+ /* Widgets declarations */
+ this.w_table_drv_temp = {
+ parent : "page_content",
+ id : "drv_temp_info",
+ type : this.OT_TABLE_H,
+ label : this.lang.drv_temp_info,
+ class_name : "tbl_info",
+ json_uri : "system/scripts/?widget=hdd_temp",
+ raw_num : true,
+ refresh : 60
+ };
+ this.w_table_drv_smart = {
+ parent : "page_content",
+ id : "drv_smart_info",
+ type : this.OT_TABLE_H,
+ label : this.lang.drv_smart_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=hdd_smart",
+ raq_num : true,
+ refresh : 3600
+ };
+ this.w_table_fs_info = {
+ parent : "page_content",
+ id : "fs_info",
+ type : this.OT_TABLE_H,
+ label : this.lang.fs_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=fs",
+ raw_num : true,
+ refresh : 30
+ };
+ this.w_table_iostat_info = {
+ parent : "page_content",
+ id : "iostat_info",
+ type : this.OT_TABLE_H,
+ label : this.lang.iostat_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=io_stat",
+ raw_num : false,
+ refresh : 30
+ };
+ this.w_table_general_info = {
+ parent : "page_content",
+ id : "general_info",
+ type : this.OT_TABLE_V,
+ label : this.lang.general_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=general_info",
+ raw_num : true,
+ refresh : 10
+ };
+ this.w_table_cpu_info = {
+ parent : "page_content",
+ id : "cpu_info",
+ type : this.OT_TABLE_V,
+ label : this.lang.cpu_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=cpu_info",
+ raw_num : false,
+ refresh : 0
+ };
+ this.w_table_memory_info = {
+ parent : "page_content",
+ id : "memory_info",
+ type : this.OT_TABLE_V,
+ label : this.lang.memory_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=mem_info",
+ raw_num : false,
+ refresh : 0
+ };
+ this.w_table_processes = {
+ parent : "page_content",
+ id : "proc_info",
+ type : this.OT_TABLE_H,
+ label : this.lang.proc_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=processes",
+ raw_num : false,
+ refresh : 15
+ };
+ this.w_table_arp_cache = {
+ parent : "page_content",
+ id : "net_arp_cache_info",
+ type : this.OT_TABLE_H,
+ label : this.lang.net_arp_cache_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=arp_cache",
+ raw_num : true,
+ refresh : 0
+ };
+ this.w_table_ip_addr = {
+ parent : "page_content",
+ id : "net_ip_addr_info",
+ type : this.OT_TABLE_H,
+ label : this.lang.net_ip_addr_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=ip_addr",
+ raw_num : true,
+ refresh : 0
+ };
+ this.w_table_ip_route = {
+ parent : "page_content",
+ id : "net_ip_route_info",
+ type : this.OT_TABLE_H,
+ label : this.lang.net_ip_route_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=ip_route",
+ raw_num : true,
+ refresh : 0
+ };
+ this.w_table_net_band = {
+ parent : "page_content",
+ id : "net_band_info",
+ type : this.OT_TABLE_H,
+ label : this.lang.net_band_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=net_band",
+ raw_num : false,
+ refresh : 15
+ };
+ this.w_table_net_conn = {
+ parent : "page_content",
+ id : "net_conn_info",
+ type : this.OT_TABLE_H,
+ label : this.lang.net_conn_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=net_conn",
+ raw_num : true,
+ refresh : 15
+ };
+ this.w_table_net_mcast = {
+ parent : "page_content",
+ id : "net_mcast_info",
+ type : this.OT_TABLE_H,
+ label : this.lang.net_mcast_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=net_multicast",
+ raw_num : true,
+ refresh : 0
+ };
+ this.w_table_lstn_socks = {
+ parent : "page_content",
+ id : "net_lstn_socks_info",
+ type : this.OT_TABLE_H,
+ label : this.lang.net_lstn_socks_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=listen_socks",
+ raw_num : true,
+ refresh : 0
+ };
+ this.w_table_unix_socks = {
+ parent : "page_content",
+ id : "net_unix_socks_info",
+ type : this.OT_TABLE_H,
+ label : this.lang.net_unix_socks_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=active_unix_socks",
+ raw_num : true,
+ refresh : 0
+ };
+ this.w_table_transmission = {
+ parent : "page_content",
+ id : "transmission_info",
+ type : this.OT_TABLE_H,
+ label : this.lang.transmission_info,
+ class_name : "tbl_info",
+ json_uri : "system/scripts/json_transmission.php",
+ raw_num : false,
+ refresh : 30
+ };
+ this.w_table_users_online = {
+ parent : "page_content",
+ id : "users_online_info",
+ type : this.OT_TABLE_H,
+ label : this.lang.users_online_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=users_online",
+ raw_num : true,
+ refresh : 30
+ };
+ this.w_table_smb_shares = {
+ parent : "page_content",
+ id : "smb_shares_info",
+ type : this.OT_TABLE_H,
+ label : this.lang.smb_shares_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=smb_shares",
+ raw_num : true,
+ refresh : 30
+ };
+ this.w_table_smb_proc = {
+ parent : "page_content",
+ id : "smb_proc_info",
+ type : this.OT_TABLE_H,
+ label : this.lang.smb_proc_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=smb_proc",
+ raw_num : true,
+ refresh : 30
+ };
+ this.w_table_smb_locks = {
+ parent : "page_content",
+ id : "smb_locks_info",
+ type : this.OT_TABLE_H,
+ label : this.lang.smb_locks_info,
+ class_name : "tbl_info",
+ json_uri : "system/json/?widget=smb_locks",
+ raw_num : true,
+ refresh : 30
+ };
+ this.w_canvas_cpu_graph = {
+ parent : "page_content",
+ id : "cpu_graph",
+ type : this.OT_CANVAS,
+ label : this.lang.cpu_info,
+ class_name : "canvas_info",
+ json_uri : "system/json/?widget=cpu_load",
+ refresh : 5,
+ min_val : 0,
+ max_val : 100,
+ raw_num : true,
+ measure : "%"
+ };
+ this.w_canvas_mem_graph = {
+ parent : "page_content",
+ id : "mem_graph",
+ type : this.OT_CANVAS,
+ label : this.lang.memory_info,
+ class_name : "canvas_info",
+ json_uri : "system/json/?widget=mem",
+ refresh : 5,
+ min_val : 0,
+ max_val : "auto",
+ raw_num : true,
+ measure : "MB"
+ };
+ this.w_canvas_net_down_graph = {
+ parent : "page_content",
+ id : "net_down_graph",
+ type : this.OT_CANVAS,
+ label : this.lang.net_down_info,
+ class_name : "canvas_info",
+ json_uri : "system/json/?widget=net_download_transfer_rate",
+ refresh : 5,
+ min_val : 0,
+ max_val : "auto",
+ raw_num : true,
+ measure : "KB/s"
+ };
+ this.w_canvas_net_up_graph = {
+ parent : "page_content",
+ id : "net_up_graph",
+ type : this.OT_CANVAS,
+ label : this.lang.net_up_info,
+ class_name : "canvas_info",
+ json_uri : "system/json/?widget=net_upload_transfer_rate",
+ refresh : 5,
+ min_val : 0,
+ max_val : "auto",
+ raw_num : true,
+ measure : "KB/s"
+ };
+ this.w_canvas_swap_graph = {
+ parent : "page_content",
+ id : "swap_graph",
+ type : this.OT_CANVAS,
+ label : this.lang.swap_info,
+ class_name : "canvas_info",
+ json_uri : "system/json/?widget=swap",
+ refresh : 5,
+ min_val : 0,
+ max_val : "auto",
+ raw_num : true,
+ measure : "MB"
+ };
+
+ this.w_table_files_check = {
+ parent : "page_content",
+ id : "files_check",
+ type : this.OT_TABLE_H,
+ label : this.lang.fs_check,
+ class_name : "tbl_info",
+ raw_num : true,
+ json_uri : "system/scripts/check_files.php"
+ };
+
+ this.w_reboot_iframe = {
+ parent : "page_content",
+ id : "reboot_iframe",
+ type : this.OT_IFRAME,
+ label : this.lang.reboot,
+ class_name : "app_widget",
+ json_uri : "system/action/?lang=" + $_GET("lang") + "&do=reboot"
+ };
+ this.w_shutdown_iframe = {
+ parent : "page_content",
+ id : "shutdown_iframe",
+ type : this.OT_IFRAME,
+ label : this.lang.shutdown,
+ class_name : "app_widget",
+ json_uri : "system/action/?lang=" + $_GET("lang") + "&do=shutdown"
+ };
+
+ this.w_settings_iframe = {
+ parent : "page_content",
+ id : "iframe_settings",
+ type : this.OT_IFRAME,
+ label : this.lang.settings,
+ class_name : "app_widget",
+ json_uri : "system/ui/forms/settings/?lang=" + $_GET("lang")
+ };
+
+ this.w_about_iframe = {
+ parent : "page_content",
+ id : "iframe_cp_about",
+ type : this.OT_IFRAME,
+ label : this.lang.help,
+ class_name : "app_widget",
+ json_uri : "system/help/?lang=" + $_GET("lang") + "#about"
+ };
+ this.w_help_iframe = {
+ parent : "page_content",
+ id : "iframe_cp_help",
+ type : this.OT_IFRAME,
+ label : this.lang.help,
+ class_name : "app_widget",
+ json_uri : "system/help/?lang=" + $_GET("lang")
+ };
+
+ /* Widgets lists */
+ this.widgets_lists = {
+ "storage": [
+ {
+ "page_title" : this.lang.storage
+ },
+ this.w_table_drv_temp,
+ this.w_table_drv_smart,
+ this.w_table_fs_info,
+ this.w_table_iostat_info
+ ],
+ "network": [
+ {
+ "page_title" : this.lang.network
+ },
+ this.w_canvas_net_down_graph,
+ this.w_canvas_net_up_graph,
+ {
+ "break" : true
+ },
+ this.w_table_ip_addr,
+ this.w_table_net_band,
+ this.w_table_ip_route,
+ this.w_table_users_online,
+ this.w_table_net_mcast,
+ this.w_table_arp_cache,
+ this.w_table_lstn_socks,
+ {
+ "widget_css" : "div_widget_full_width"
+ },
+ this.w_table_unix_socks,
+ {
+ "widget_css" : "div_widget_full_width"
+ },
+ this.w_table_net_conn,
+ this.w_table_transmission
+ ],
+ "smb": [
+ {
+ "page_title" : this.lang.smb
+ },
+ this.w_table_smb_proc,
+ this.w_table_smb_shares,
+ {
+ "widget_css" : "div_widget_full_width"
+ },
+ this.w_table_smb_locks
+ ],
+ "system": [
+ {
+ "page_title" : this.lang.system,
+ "widget_css" : "div_widget_type_a"
+ },
+ this.w_table_general_info,
+ this.w_table_memory_info,
+ this.w_table_cpu_info
+ ],
+ "system_pulse": [
+ {
+ "page_title" : this.lang.system_pulse
+ },
+ this.w_canvas_cpu_graph,
+ this.w_canvas_mem_graph,
+ this.w_canvas_swap_graph,
+ this.w_canvas_net_down_graph,
+ this.w_canvas_net_up_graph,
+ {
+ "widget_css" : "div_widget_full_width"
+ },
+ this.w_table_processes
+ ]
+ };
+
+ this.server_name_fill();
+ this.language_selector_add();
+
+ var m_menu = new MainMenu(this.lang);
+
+ m_menu.clear_menu(m_menu.main_menu);
+ m_menu.build_menu(this.menu);
+
+ this.apps_list_load();
+ // app.check_apps();
+ if (window.location.hash.substr(1) !== "") {
+ if (window.location.hash.substr(1) === "settings") {
+ this.settings_show();
+ } else if (window.location.hash.substr(1) === "cp_help") {
+ this.help_show(false);
+ } else if (window.location.hash.substr(1) === "cp_about") {
+ this.help_show(true);
+ } else if (window.location.hash.substr(1, 3) !== "app") {
+ this.widgets_run(window.location.hash.substr(1));
+ if (window.location.hash.substr(1) === "system") {
+ this.os_logo_draw();
+ }
+ } else {
+ this.app_execute(window.location.hash.substr(1));
+ }
+ } else {
+ this.widgets_run("system");
+ this.os_logo_draw();
+ }
+
+ /* Enable S.M.A.R.T and temperature monitoring */
+ this.monitoring_enable();
+
+ return true;
+ }
+ };
+
+ /**
+ * Add language selector to page's header
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.language_selector_add = function () {
+ var header = document.getElementById("app_header"),
+ select_lang = document.createElement("select");
+
+ select_lang.id = "lang";
+ select_lang.classList.add("select_lang");
+
+ if ($_GET("lang")) {
+ select_lang.classList.add("input_" + $_GET("lang"));
+ } else {
+ select_lang.classList.add("input_" + this.settings.lang);
+ }
+
+ select_lang.onchange = function () {
+ /*
+ * window.open(URL,name,specs,replace): where name:
+ *
+ * _blank - URL is loaded into a new window. This is default
+ * _parent - URL is loaded into the parent frame
+ * _self - URL replaces the current page
+ * _top - URL replaces any framesets that may be loaded
+ * name - The name of the window (Note: the name does not specify
+ * the title of the new window)
+ */
+ window.open(this.options[this.selectedIndex].value, "_self");
+ };
+
+ for (var language in this.settings.langs) {
+ if (this.settings.langs.hasOwnProperty(language)) {
+ var opt_lang = document.createElement("option");
+
+ opt_lang.value = "?lang=" + language;
+ opt_lang.appendChild(document.createTextNode(this.settings.langs[language]));
+
+ if ($_GET("lang") && $_GET("lang") === language) {
+ opt_lang.selected = true;
+ } else if (!$_GET("lang") && this.settings.lang === language){
+ opt_lang.selected = true;
+ }
+
+ select_lang.appendChild(opt_lang);
+ }
+ }
+ header.appendChild(select_lang);
+ };
+
+ /**
+ * Change page title
+ *
+ * @param {string} new_title New page title
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.page_title_change = function (new_title) {
+ var srv_title = document.getElementById("s_header").textContent;
+
+ document.title = document.getElementById("s_header").textContent =
+ srv_title.substring(0, srv_title.indexOf("#") + 1) + " " + new_title;
+ };
+
+ /**
+ * Check application existance and remove from menu if unexisted
+ *
+ * @param {string} app_name Application name
+ * @param {string} app_exe Application executable
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.app_remove_unexisted = function (app_name, app_exe) {
+ this.get_data("system/json/?widget=check_app¶m=" + app_exe)
+ .then(function(result) {
+ var element = document.getElementById("div_menu_" + app_name),
+ json_data = JSON.parse(result);
+
+ if ((json_data.exec !== "") && (!json_data.installed)) {
+ element.parentNode.removeChild(element);
+ } else {
+ element.classList.remove("hidden");
+ }
+ })
+ .catch(function(result) {
+ console_log("app_remove_unexisted error: " + result);
+ });
+ };
+
+ /**
+ * Draw OS distro logo
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.os_logo_draw = function () {
+ this.get_data("system/json/?widget=os_distr")
+ .then(function(result) {
+ var json_data = JSON.parse(result),
+ div_gi = document.getElementById("general_info");
+
+ if (!div_gi) {
+ return;
+ }
+
+ var img_logo = document.createElement("img");
+
+ img_logo.classList.add("distro_logo");
+
+ if (json_data.Distr.match(/opensuse/i)) {
+ img_logo.src = "gfx/distros/opensuse.png";
+ } else if (json_data.Distr.match(/arch/i)) {
+ img_logo.src = "gfx/distros/arch.png";
+ } else if (json_data.Distr.match(/bsd/i)) {
+ img_logo.src = "gfx/distros/bsd.png";
+ } else if (json_data.Distr.match(/debian/i)) {
+ img_logo.src = "gfx/distros/debian.png";
+ } else if (json_data.Distr.match(/fedora/i)) {
+ img_logo.src = "gfx/distros/fedora.png";
+ } else if (json_data.Distr.match(/ubuntu/i)) {
+ img_logo.src = "gfx/distros/ubuntu.png";
+ } else if (json_data.Family.match(/linux/i)) {
+ img_logo.src = "gfx/distros/linux.png";
+ } else if (json_data.Family.match(/darwin/i)) {
+ img_logo.src = "gfx/distros/apple.png";
+ } else {
+ img_logo.src = "gfx/distros/unknown.png";
+ }
+
+ div_gi.appendChild(img_logo);
+ })
+ .catch(function(result) {
+ console_log("Can't get disto info: " + result);
+ });
+ };
+
+ /**
+ * Clear page content
+ *
+ * returns {undefined}
+ */
+ Application.prototype.page_clear = function () {
+ var parent_node = document.getElementById("page_content");
+
+ /* Clear timers */
+ for (var key in this.timers) {
+ if (this.timers.hasOwnProperty(key)) {
+ clearInterval(this.timers[key]);
+ }
+ }
+ this.timers = [];
+
+ /* Remove widgets */
+ while (parent_node.firstChild) {
+ parent_node.removeChild(parent_node.firstChild);
+ }
+ };
+
+ /**
+ * Open control panel with selected application or widgets screen in new
+ * window
+ *
+ * @param {string} hash URL hash
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.app_new_window = function (hash) {
+ var new_location = location.href.replace(location.hash,""),
+ new_win = window.open(new_location + "#" + hash, "_blank");
+
+ new_win.focus();
+
+ return;
+ };
+
+ /**
+ * Draw application widget
+ *
+ * @param {object} w_info Widget info
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.app_widget_draw = function (w_info) {
+ var app_hash = w_info.id.substring(7);
+
+ if (this.ctrl_on) {
+ this.app_new_window(app_hash);
+ return;
+ }
+
+ var app_widget = new Widget(this, w_info);
+
+ this.page_clear();
+ this.page_title_change(w_info.label);
+
+ app_widget.draw();
+
+ location.hash = app_hash;
+
+ document.getElementById(w_info.id).classList.remove("slightly_visible");
+ };
+
+ /**
+ * Fill server name on HTML page
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.server_name_fill = function () {
+ this.get_data("system/json/?widget=srv_name")
+ .then(function(result) {
+ var s_header = document.getElementById("s_header"),
+ json_data = JSON.parse(result);
+
+ if (s_header.textContent !== "Loading...") {
+ var temp = s_header.textContent;
+
+ s_header.removeChild(s_header.childNodes[0]);
+ s_header.appendChild(document.createTextNode(
+ json_data.server_name + " #" + temp));
+ } else {
+ s_header.removeChild(s_header.childNodes[0]);
+ s_header.appendChild(document.createTextNode(
+ json_data.server_name + " #"));
+ }
+
+ document.title = s_header.textContent;
+ })
+ .catch(function(result) {
+ console_log("server_name_fill error: " + result);
+ });
+ };
+
+ /**
+ * Execute application
+ *
+ * @param {string} app_name application name
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.app_execute = function (app_name) {
+ var app_instance = this;
+
+ this.get_data("system/apps/" + app_name.substr(4) + "/app.json")
+ .then(function(result) {
+// TODO: Check why https_uri
+ var json_data = JSON.parse(result),
+ uri = json_data.https_uri,
+ app_iframe = {
+ parent : "page_content",
+ id : "iframe_" + json_data.app_name,
+ type : app_instance.OT_IFRAME,
+ label : json_data.caption,
+ class_name : "app_widget",
+ json_uri : uri.replace("[server_name]",
+ window.location.hostname)
+ };
+
+ app_instance.app_widget_draw(app_iframe);
+ })
+ .catch(function(result) {
+ console_log("app_execute error: " + result);
+ });
+ };
+
+ /**
+ * Enable HDD S.M.A.R.T. monitoring
+ *
+ * @param {numeric} timeout Timeout in seconds
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.monitor_hdd_smart = function (timeout) {
+ var app_instance = this;
+
+ setTimeout(function () {
+ app_instance.get_data("system/json/?widget=hdd_smart")
+ .then(function(result) {
+ var hdd_smart = JSON.parse(result),
+ cur_date = new Date();
+
+ for (var hdd in hdd_smart) {
+ if ((hdd_smart[hdd].status.toLowerCase() !== "passed") &&
+ (hdd_smart[hdd].status.toLowerCase() !== "ok")) {
+ app_instance.notify(app_instance.lang.attention,
+ {body: cur_date.toLocaleString() + ": " +
+ app_instance.lang.hdd_smart_error + " " +
+ hdd_smart[hdd].drive + " (" +
+ hdd_smart[hdd].status + ")",
+ tag: "cai_cp",
+ icon: "../gfx/icons/attention.png",
+ dir: "auto"},
+ function () {
+ app_instance.widgets_run("storage");
+ });
+ }
+ }
+
+ console_log("HDD SMART monitor");
+ app_instance.monitor_hdd_smart(timeout);
+ })
+ .catch(function(result) {
+ console_log("monitor_hdd_smart error: " + result);
+ app_instance.monitor_hdd_smart(timeout);
+ });
+ },
+ timeout * 1000);
+ };
+
+ /**
+ * Enable HDD temperature monitoring
+ *
+ * @param {numeric} timeout Timeout in seconds
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.monitor_hdd_temp = function (timeout) {
+ var app_instance = this;
+
+ setTimeout(function () {
+ app_instance.get_data("system/scripts/?widget=hdd_temp")
+ .then(function(result) {
+ var hdd_temp = JSON.parse(result),
+ cur_date = new Date();
+
+ for (var hdd in hdd_temp) {
+ if (hdd_temp[hdd].temperature.substring(0,
+ hdd_temp[hdd].temperature.length - 2) > app_instance.settings.max_hdd_temp) {
+ app_instance.notify(app_instance.lang.attention,
+ {body: cur_date.toLocaleString() + ": " +
+ app_instance.lang.hdd_overheat + " " +
+ hdd_temp[hdd].drive + " (" +
+ hdd_temp[hdd].model + "): " +
+ hdd_temp[hdd].temperature,
+ tag: "cai_cp",
+ icon: "../gfx/icons/attention.png",
+ dir: "auto"},
+ function () {
+ app_instance.widgets_run("storage");
+ });
+ }
+ }
+
+ console_log("HDD temperature monitor");
+ app_instance.monitor_hdd_temp(timeout);
+ })
+ .catch(function(result) {
+ console_log("monitor_hdd_temp error: " + result);
+ app_instance.monitor_hdd_temp(timeout);
+ });
+ },
+ timeout * 1000);
+ };
+
+ /**
+ * Enable online users monitoring
+ *
+ * @param {numeric} timeout Timeout in seconds
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.monitor_users_online = function (timeout) {
+ var app_instance = this;
+
+ setTimeout(function () {
+ app_instance.get_data("system/scripts/?widget=users_online")
+ .then(function(result) {
+ var users_online = JSON.parse(result),
+ cur_date = new Date();
+
+ if (users_online.length > app_instance.mon_users_info.count ||
+ users_online[users_online.length - 1].from !== app_instance.mon_users_info.last_addr ||
+ users_online[users_online.length - 1].user !== app_instance.mon_users_info.last_user) {
+ app_instance.notify(app_instance.lang.attention,
+ {body: cur_date.toLocaleString() + ": " +
+ app_instance.lang.new_user_connected +
+ " (" +
+ users_online[users_online.length - 1].user +
+ ", " +
+ users_online[users_online.length - 1].from +
+ ")",
+ tag: "cai_cp",
+ icon: "../gfx/icons/attention.png",
+ dir: "auto"},
+ function () {
+ app_instance.widgets_run("network");
+ });
+ }
+ app_instance.mon_users_info.count = users_online.length;
+ app_instance.mon_users_info.last_user = users_online[users_online.length - 1].user;
+ app_instance.mon_users_info.last_addr = users_online[users_online.length - 1].from;
+
+ console_log("Online users monitor");
+ app_instance.monitor_users_online(timeout);
+ })
+ .catch(function(result) {
+ console_log("monitor_users_online error: " + result);
+ app_instance.monitor_users_online(timeout);
+ });
+ },
+ timeout * 1000);
+ };
+
+ /**
+ * Enable monitoring functions
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.monitoring_enable = function () {
+ /* Set HDD temperature monitoring */
+ if (isNaN(this.settings.check_hdd_temp_interval)) {
+ this.settings.check_hdd_temp_interval = 0;
+ } else {
+ this.monitor_hdd_temp(this.settings.check_hdd_temp_interval);
+ }
+
+ /* Set S.M.A.R.T. monitoring */
+ if (isNaN(this.settings.check_smart_interval)) {
+ this.settings.check_smart_interval = 0;
+ } else {
+ this.monitor_hdd_smart(this.settings.check_smart_interval);
+ }
+
+ /* Set connected users monitoring */
+ if (isNaN(this.settings.check_users_online_interval)) {
+ this.settings.check_users_online_interval = 0;
+ } else {
+ this.monitor_users_online(this.settings.check_users_online_interval);
+ }
+ };
+
+ /**
+ * Show browser notification
+ *
+ * @param {string} title Notigication title
+ * @param {object} options Notification options
+ * @param {function} callback Callback function
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.notify = function (title, options, callback) {
+ if (!("Notification" in window) ||
+ (Notification.permission.toLowerCase() === "denied")) {
+ /* Notifications not supported or denied */
+ return;
+ }
+
+ if (Notification.permission === "granted") { // Notifications allowed
+ var notification = new Notification(title, options);
+
+ notification.onclick = callback;/* function () {
+ console.log("notification clicked");
+ }; */
+ } else { // Trying to get permissions
+ Notification.requestPermission(function (permission) {
+ if (permission === "granted") {
+ var notification = new Notification(title, options);
+ } /*else {
+ console.log("notifications prohibited");
+ } */
+ });
+ }
+ };
+
+ /**
+ * Register applications in menu
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.apps_register = function () {
+ var m_menu = new MainMenu(this.lang);
+
+ for (var key in this.apps_list) {
+ if (this.apps_list.hasOwnProperty(key)) {
+ m_menu.register_app(this, this.apps_list[key]);
+ document.getElementById("div_menu_app").classList.remove("hidden");
+ }
+ }
+ };
+
+ /**
+ * Load applications list
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.apps_list_load = function () {
+ var app_instance = this;
+
+ this.get_data("system/apps/apps.json")
+ .then(function(result) {
+ var json_data = JSON.parse(result);
+ app_instance.apps_list = json_data;
+ app_instance.apps_register();
+ })
+ .catch(function(result) {
+ console_log("apps_list_load error:" + result);
+ });
+ };
+
+ /**
+ * Run widget
+ *
+ * @param {object} widget_info Widget description
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.widget_run = function (widget_info) {
+ Object.size = function(obj) {
+ var size = 0, key;
+ for (key in obj) {
+ if (obj.hasOwnProperty(key)) size++;
+ }
+ return size;
+ };
+
+ var w_index = Object.size(this.widgets); //this.widgets.length;
+
+ this.widgets[w_index] = new Widget(this, widget_info);
+ this.widgets[w_index].draw(this.widgets[w_index]);
+
+ if (widget_info.type === this.OT_TABLE_H ||
+ widget_info.type === this.OT_TABLE_V) {
+ this.widgets[w_index].update_table_data(this, w_index);
+ if (widget_info.refresh > 0) {
+ this.timers[widget_info.id] =
+ setInterval(this.widgets[w_index].update_table_data,
+ widget_info.refresh * 1000,
+ this, w_index);
+ }
+ } else if (widget_info.type === this.OT_CANVAS) {
+ this.widgets[w_index].update_canvas_data(this, w_index);
+ if (widget_info.refresh > 0) {
+ this.timers[widget_info.id] =
+ setInterval(this.widgets[w_index].update_canvas_data,
+ widget_info.refresh * 1000,
+ this, w_index);
+ }
+ }
+ };
+
+ /**
+ * Draws application widgets
+ *
+ * @param {array} block_name Name of widgets block
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.widgets_run = function (block_name) {
+ /* Open in new window if ctrl is pressed */
+ if (this.ctrl_on) {
+ this.app_new_window(block_name);
+ return;
+ }
+
+ if (typeof block_name === "undefined") {
+ return;
+ }
+
+ this.cur_block = this.blocks_rotation.indexOf(block_name);
+
+ var class_name = "div_widget",
+ no_widgets = false;
+
+ this.page_clear();
+
+ location.hash = block_name;
+
+ this.widgets = {};
+
+ for (var i = 0; i < this.widgets_lists[block_name].length; i++) {
+ no_widgets = false;
+
+ if (this.widgets_lists[block_name][i].widget_css !== undefined) {
+ /* Apply widget css */
+ no_widgets = true;
+ class_name = this.widgets_lists[block_name][i].widget_css;
+ } else if (this.widgets_lists[block_name][i].break !== undefined &&
+ this.widgets_lists[block_name][i].break === true) {
+ /* Insert break */
+ var div_break = document.createElement("div");
+ div_break.className = "div_clear";
+
+ document.getElementById("page_content").appendChild(div_break);
+
+ no_widgets = true;
+ }
+
+ if (this.widgets_lists[block_name][i].page_title !== undefined) {
+ this.page_title_change(this.widgets_lists[block_name][i].page_title);
+
+ no_widgets = true;
+ }
+
+ /* If widget info */
+ if (!no_widgets) {
+ if (!this.settings.w_transmission_enabled &&
+ this.widgets_lists[block_name][i].id === "transmission_info") {
+ /* Skip transmission widget if disabled */
+ } else {
+ this.widget_run(this.widgets_lists[block_name][i]);
+ /* Apply widget style class */
+ if (class_name !== "div_widget") {
+ document.getElementById(this.widgets_lists[block_name][i].id).classList.remove("div_widget");
+ document.getElementById(this.widgets_lists[block_name][i].id).classList.add(class_name);
+ }
+ }
+ }
+ }
+ };
+
+ /**
+ * Show help
+ *
+ * @param {bool} about_info Show "About" info
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.help_show = function (about_info) {
+ if (!this.ctrl_on) {
+ this.page_clear();
+ }
+
+ if (about_info) {
+ this.app_widget_draw(this.w_about_iframe);
+ } else {
+ this.app_widget_draw(this.w_help_iframe);
+ }
+ };
+
+ /**
+ * Show settings form
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.settings_show = function () {
+ if (!this.ctrl_on) {
+ this.page_clear();
+ }
+
+ this.app_widget_draw(this.w_settings_iframe);
+ };
+
+ /**
+ * Shutdown server
+ *
+ * @param {bool} reboot Reboot system flag
+ *
+ * @returns {undefined}
+ */
+ Application.prototype.shutdown = function (reboot) {
+ if (reboot && window.confirm (this.lang.reboot_confirm)) {
+ this.page_clear();
+ this.app_widget_draw(this.w_reboot_iframe);
+ } else if (!reboot && window.confirm (this.lang.shutdown_confirm)) {
+ this.page_clear();
+ this.app_widget_draw(this.w_shutdown_iframe);
+ }
+ };
+
+ /**
+ * Main menu constructor
+ *
+ * @param {string} lang Language
+ *
+ * @returns {undefined}
+ */
+ function MainMenu(lang) {
+ this.main_menu = document.getElementById("div_menu");
+ this.info_menu = document.getElementById("div_menu_info");
+ this.app_menu = document.getElementById("div_menu_app");
+ this.settings_menu = document.getElementById("div_menu_settings");
+ this.lang = lang;
+ }
+
+ /**
+ * Add icons block to menu
+ *
+ * @param {array} block_info Menu block information
+ *
+ * @returns {object} Menu block's div
+ */
+ MainMenu.prototype.add_block = function (block_info) {
+ var div_menu_block = document.createElement("div"),
+ div_menu_block_caption = document.createElement("div");
+
+ div_menu_block.id = block_info.id;
+ div_menu_block.classList.add("div_menu_block");
+
+ if (!block_info.visible) {
+ div_menu_block.classList.add("hidden");
+ }
+
+ this.main_menu.appendChild(div_menu_block);
+
+ div_menu_block_caption.classList.add("div_menu_block_caption");
+ div_menu_block_caption.appendChild(document.createTextNode(block_info.caption));
+ div_menu_block.appendChild(div_menu_block_caption);
+
+ if (block_info.separator) {
+ div_menu_block.appendChild(document.createElement("hr"));
+ }
+
+ return div_menu_block;
+ };
+
+ /**
+ * Add menu item to menu block
+ *
+ * @param {object} parent_block Parent block for icon
+ * @param {string} id Menu item id
+ * @param {string} icon_src Path to icon file
+ * @param {string} caption Menu caption
+ * @param {bool} hidden Add menu in hidden state
+ * @param {object} callback_function OnClick callback function
+ *
+ * @returns {undefined}
+ */
+ MainMenu.prototype.add_item = function (parent_block,
+ id,
+ icon_src,
+ caption,
+ hidden,
+ callback_function) {
+ var m_item_div = document.createElement("div"),
+ m_item_img = document.createElement("img"),
+ m_item_footer_div = document.createElement("div");
+
+ m_item_div.id = "div_" + id;
+ m_item_div.classList.add("div_m_item");
+ m_item_div.onclick = callback_function;
+/*
+m_item_div.addEventListener("contextmenu", function(e) {
+ e.preventDefault();
+});
+*/
+ if (hidden) {
+ m_item_div.classList.add("hidden");
+ }
+
+ m_item_img.classList.add("menu_icon");
+ m_item_img.src = icon_src;
+ m_item_div.appendChild(m_item_img);
+
+ m_item_footer_div.classList.add("div_m_item_footer");
+ m_item_footer_div.appendChild(document.createTextNode(caption));
+ m_item_div.appendChild(m_item_footer_div);
+
+ parent_block.appendChild(m_item_div);
+ };
+
+ /**
+ * Build menu structure
+ *
+ * @param {object} menu_items menu structure
+ *
+ * @returns {undefined}
+ */
+ MainMenu.prototype.build_menu = function (menu_items) {
+ var div_ctrl_hint = document.createElement("div");
+
+ div_ctrl_hint.classList.add("top_right_hint");
+ div_ctrl_hint.appendChild(document.createTextNode(this.lang.ctrl_click_hint));
+
+ this.main_menu.appendChild(div_ctrl_hint);
+
+ for (var menu_block in menu_items) {
+ if (menu_items.hasOwnProperty(menu_block)) {
+ var div_menu_block = this.add_block(menu_items[menu_block]);
+
+ for (var menu_item in menu_items[menu_block].elements) {
+ if (menu_items[menu_block].elements.hasOwnProperty(menu_item)) {
+ this.add_item(
+ div_menu_block,
+ menu_item,
+ menu_items[menu_block].elements[menu_item].icon,
+ menu_items[menu_block].elements[menu_item].caption,
+ false,
+ menu_items[menu_block].elements[menu_item].callback
+ );
+ }
+ }
+
+ var div_clear = document.createElement("div");
+
+ div_clear.classList.add("div_clear");
+ div_menu_block.appendChild(div_clear);
+
+ this.main_menu.appendChild(div_menu_block);
+ }
+ }
+ };
+
+ /**
+ * Clear menu or submenu
+ *
+ * @param {object} menu Menu object to clear
+ *
+ * @returns {undefined}
+ */
+ MainMenu.prototype.clear_menu = function (menu) {
+ while (menu.firstChild) {
+ menu.removeChild(menu.firstChild);
+ }
+ };
+
+ /**
+ * Register application in menu
+ *
+ * @param {object} app_instance Application instance
+ * @param {string} app_dir Directory containing app.json file with
+ * application info
+ *
+ * @returns {undefined}
+ */
+ MainMenu.prototype.register_app = function (app_instance, app_dir) {
+ var menu_obj = this;
+
+ app_instance.get_data("system/apps/" + app_dir + "/app.json")
+ .then(function(result) {
+ var json_data = JSON.parse(result),
+ parent_block = document.getElementById("div_menu_app"),
+ uri,
+ app_iframe;
+
+ switch (window.location.protocol) {
+ case "https:":
+ if (json_data.use_https && json_data.https_uri !== "") {
+ if (json_data.frame_support) {
+ uri = json_data.https_uri;
+ /* run in frame */
+ app_iframe = {
+ parent : "page_content",
+ id : "iframe_" + json_data.app_name,
+ type : app_instance.OT_IFRAME,
+ label : json_data.caption,
+ class_name : "app_widget",
+ json_uri : uri.replace("[server_name]",
+ window.location.hostname)
+ };
+ menu_obj.add_item(parent_block,
+ "menu_" + json_data.app_name,
+ "system/apps/" + app_dir + "/icon.png",
+ json_data.caption,
+ json_data.exec !== "",
+ function () {
+ app_instance.app_widget_draw(app_iframe);
+ }
+ );
+ } else {
+ menu_obj.add_item(parent_block,
+ "menu_" + json_data.app_name,
+ "system/apps/" + app_dir + "/icon.png",
+ json_data.caption,
+ json_data.exec !== "",
+ function () {
+ var uri = json_data.https_uri;
+
+ window.open(uri.replace("[server_name]",
+ window.location.hostname),
+ "_blank");
+ }
+ );
+ }
+ } else if (json_data.use_http && json_data.http_uri !== "") {
+ menu_obj.add_item(parent_block,
+ "menu_" + json_data.app_name,
+ "system/apps/" + app_dir + "/icon.png",
+ json_data.caption,
+ json_data.exec !== "",
+ function () {
+ var uri = json_data.http_uri;
+
+ window.open(uri.replace("[server_name]",
+ window.location.hostname),
+ "_blank");
+ }
+ );
+ }
+ break;
+ case "http:":
+ if (json_data.use_http && json_data.http_uri !== "") {
+ if (json_data.frame_support) {
+ uri = json_data.http_uri;
+ /* run in frame */
+ app_iframe = {
+ parent : "page_content",
+ id : "iframe_" + json_data.app_name,
+ type : app_instance.OT_IFRAME,
+ label : json_data.caption,
+ class_name : "app_widget",
+ json_uri : uri.replace("[server_name]",
+ window.location.hostname)
+ };
+ menu_obj.add_item(parent_block,
+ "menu_" + json_data.app_name,
+ "system/apps/" + app_dir + "/icon.png",
+ json_data.caption,
+ json_data.exec !== "",
+ function () {
+ app_instance.app_widget_draw(app_iframe);
+ }
+ );
+ } else {
+ menu_obj.add_item(parent_block,
+ "menu_" + json_data.app_name,
+ "system/apps/" + app_dir + "/icon.png",
+ json_data.caption,
+ json_data.exec !== "",
+ function () {
+ var uri = json_data.http_uri;
+
+ window.open(uri.replace("[server_name]",
+ window.location.hostname),
+ "_blank");
+ }
+ );
+ }
+ } else if (json_data.use_https && json_data.https_uri !== "") {
+ menu_obj.add_item(parent_block,
+ "menu_" + json_data.app_name,
+ "system/apps/" + app_dir + "/icon.png",
+ json_data.caption,
+ json_data.exec !== "",
+ function () {
+ var uri = json_data.https_uri;
+
+ window.open(uri.replace("[server_name]",
+ window.location.hostname),
+ "_blank");
+ }
+ );
+ }
+ break;
+ }
+ for (var i = 0; i < json_data.require.length; i++) {
+ app_instance.app_remove_unexisted(json_data.app_name,
+ json_data.require[i].data);
+ }
+ })
+ .catch(function(result) {
+ console_log("register_app error: " + result);
+ });
+ };
+
+ /**
+ * Widgets constructor
+ *
+ * @param {object} caller Caller object
+ * @param {array} w_info Array with Widget information
+ * @param {string} w_data Widget data
+ *
+ * @returns {undefined}
+ */
+ function Widget(caller, w_info, w_data) {
+ this.parent_app = caller;
+ this.info = w_info;
+ this.data = w_data || null;
+ this.parent_el = document.getElementById(w_info.parent);
+
+ /* Double tap on header handler vars */
+ this.dt_hdr_timeout = null;
+ this.dt_hdr_tap_time = 0;
+ }
+
+ /**
+ * Toggle maximized widget state
+ *
+ * @returns {undefined}
+ */
+ Widget.prototype.toggle_maximized = function () {
+ var parent_node = document.getElementById(this.parentNode.id);
+
+ if (parent_node.classList.contains("canvas_info")) {
+ return;
+ }
+
+ document.getElementById(this.parentNode.id).classList.toggle("div_maximized");
+
+ for (var i = 0; i < parent_node.childNodes.length; i++) {
+ if (parent_node.childNodes[i].classList.contains("div_w_content_container")) {
+ parent_node.childNodes[i].classList.toggle("div_maximized_content");
+ }
+ }
+ };
+
+ /**
+ * Draw widget
+ *
+ * @returns {undefined}
+ */
+ Widget.prototype.draw = function () {
+ var widget_instance = this,
+ widget_node,
+ widget_c_container;
+
+ if (document.getElementById(this.info.id) !== null) {
+ widget_node = document.getElementById(this.info.id);
+
+ widget_c_container = widget_node.childNodes[1];
+ widget_c_container.removeChild(widget_c_container.childNodes[0]);
+ } else {
+ var widget_head = document.createElement("div");
+
+ widget_node = document.createElement("div");
+ widget_node.classList.add("div_widget");
+
+ if (this.parent_app.settings.dim_on_create) {
+ widget_node.classList.add("slightly_visible");
+ }
+
+ widget_node.id = this.info.id;
+
+ if (this.info.class_name !== "") {
+ widget_node.classList.add(this.info.class_name);
+ }
+
+ widget_head.classList.add("div_w_head");
+ widget_head.appendChild(document.createTextNode(this.info.label));
+ widget_head.addEventListener("dblclick", this.toggle_maximized);
+ /* Add double tap event */
+ widget_head.addEventListener("touchend", function(event) {
+ var cur_time = new Date().getTime(),
+ tap_len = cur_time - this.dt_hdr_tap_time;
+
+ clearTimeout(widget_instance.dt_hdr_timeout);
+
+ if (tap_len < 500 && tap_len > 0) {
+ event.preventDefault();
+ widget_instance.toggle_maximized();
+ } else {
+ widget_instance.dt_hdr_timeout = setTimeout(function() {
+ clearTimeout(widget_instance.dt_hdr_timeout);
+ }, 500);
+ }
+
+ widget_instance.dt_hdr_tap_time = cur_time;
+ });
+
+ widget_c_container = document.createElement("div");
+ widget_c_container.classList.add("div_w_content_container");
+
+ widget_node.appendChild(widget_head);
+ widget_node.appendChild(widget_c_container);
+
+ this.parent_el.appendChild(widget_node);
+ }
+
+ var widget_content = document.createElement("div");
+
+ widget_content.classList.add("div_w_content");
+
+ widget_c_container.appendChild(widget_content);
+
+ switch (this.info.type) {
+ case this.parent_app.OT_TABLE_H:
+ case this.parent_app.OT_TABLE_V:
+ var table_node = this.html_table(this.info.type === this.parent_app.OT_TABLE_V);
+
+ if (this.info.class_name !== "") {
+ table_node.classList.add(this.info.class_name);
+ }
+ widget_content.appendChild(table_node);
+
+ break;
+ case this.parent_app.OT_IFRAME:
+ var iframe_node = document.createElement("iframe");
+
+ iframe_node.src = this.info.json_uri;
+ widget_content.appendChild(iframe_node);
+
+ break;
+ case this.parent_app.OT_CANVAS:
+ var canvas_node = document.createElement("canvas"),
+ s_settings = {interpolation: this.info.interpolation,
+ //interpolation : "linear",
+ //interpolation : "step",
+ grid : {sharpLines : true,
+ borderVisible : false,
+ verticalSections : 4,
+ millisPerLine : 10000},
+ labels : {disabled : false},
+ timestampFormatter : SmoothieChart.timeFormatter,
+ millisPerPixel : 100 //tooltip : true
+ };
+
+ canvas_node.id = "canvas_" + this.info.id;
+ canvas_node.width = 400;
+ canvas_node.height = 154;
+
+ widget_content.appendChild(canvas_node);
+
+ if (this.info.max_val !== "auto") {
+ s_settings.maxValue = this.info.max_val;
+ }
+
+ if (this.info.min_val !== "auto") {
+ s_settings.minValue = this.info.min_val;
+ }
+
+ this.smoothie = new SmoothieChart(s_settings);
+ this.lines = [];
+ this.smoothie.streamTo(document.getElementById(canvas_node.id),
+ this.info.refresh * 1000);
+ }
+ };
+
+ /**
+ * Fill table headers
+ *
+ * @param {object} table Parent table
+ *
+ * @returns {object}
+ */
+ Widget.prototype.fill_table_headers = function (table) {
+ var col_set = [];
+
+ if (this.data !== null) {
+ var tr = document.createElement("tr");
+ for (var i = 0, l = this.data.length; i < l; i++) {
+ for (var key in this.data[i]) {
+ if (this.data[i].hasOwnProperty(key) && col_set.indexOf(key) === -1) {
+ col_set.push(key);
+
+ var th = document.createElement("th");
+
+ th.appendChild(document.createTextNode(key));
+ tr.appendChild(th);
+ }
+ }
+ }
+ table.appendChild(tr);
+ }
+
+ return col_set;
+ };
+
+ /**
+ * Return HTML table filled with data
+ *
+ * @returns {object}
+ */
+ Widget.prototype.html_table = function () {
+ var table = document.createElement("table"),
+ tr;
+
+ if (this.data !== null) {
+ if (this.info.type === this.parent_app.OT_TABLE_V) {
+ var even_line = false;
+ for (var key in this.data[0]) {
+ if (this.data[0].hasOwnProperty(key)) {
+ var td_key = document.createElement("td"),
+ td_val = document.createElement("td");
+
+ tr = document.createElement("tr");
+
+ td_key.appendChild(document.createTextNode(key));
+ td_key.classList.add("table_header");
+ if (this.info.raw_num) {
+ td_val.appendChild(document.createTextNode(this.data[0][key] || ""));
+ } else {
+ td_val.appendChild(
+ document.createTextNode(
+ numFormatted(this.data[0][key], "'") || ""
+ )
+ );
+ }
+
+ tr.appendChild(td_key);
+ tr.appendChild(td_val);
+
+ if (even_line) {
+ tr.classList.add("even_line");
+ }
+
+ even_line = !even_line;
+ table.appendChild(tr);
+ }
+ }
+ } else {
+ var columns = this.fill_table_headers(table);
+
+ for (var i = 0, max_i = this.data.length; i < max_i; ++i) {
+ tr = document.createElement("tr");
+
+ for (var j = 0, max_j = columns.length; j < max_j ; ++j) {
+ var td = document.createElement("td");
+
+ if (this.info.raw_num) {
+ td.appendChild(
+ document.createTextNode(
+ this.data[i][columns[j]] || ""
+ )
+ );
+ } else {
+ td.appendChild(
+ document.createTextNode(
+ numFormatted(this.data[i][columns[j]], "'") || ""
+ )
+ );
+ }
+ tr.appendChild(td);
+ }
+
+ if (i % 2 !== 0) {
+ tr.classList.add("even_line");
+ }
+ table.appendChild(tr);
+ }
+ }
+ }
+ return table;
+ };
+
+ /**
+ * Update canvas widget data
+ *
+ * @param {object} app_instance Application object instance
+ * @param {numeric} index Widget index
+ *
+ * @returns {undefined}
+ */
+ Widget.prototype.update_canvas_data = function (app_instance, index) {
+ var w_instance = app_instance.widgets[index],
+ colors = [
+ ["rgb(0, 255, 0)", "rgba(0, 255, 0, 0.4)"], // green
+ ["rgb(255, 0, 0)", "rgba(255, 0, 0, 0.4)"], // red
+ ["rgb(255, 255, 0)", "rgba(255, 255, 0, 0.4)"], // yellow
+ ["rgb(0, 255, 255)", "rgba(0, 255, 255, 0.4)"],
+ ["rgb(0, 0, 255)", "rgba(0, 0, 255, 0.4)"], // blue
+ ["rgb(255, 255, 255)", "rgba(255, 255, 255, 0.4)"],
+ ["rgb(128, 255, 255)", "rgba(128, 255, 255, 0.4)"],
+ ["rgb(255, 128, 255)", "rgba(255, 128, 255, 0.4)"],
+ ["rgb(255, 255, 128)", "rgba(255, 255, 128, 0.4)"],
+ ["rgb(128, 128, 255)", "rgba(128, 128, 255, 0.4)"],
+ ["rgb(255, 128, 128)", "rgba(255, 128, 128, 0.4)"],
+ ["rgb(128, 255, 128)", "rgba(128, 255, 128, 0.4)"],
+ ["rgb(0, 128, 255)", "rgba(0, 128, 255, 0.4)"],
+ ["rgb(255, 0, 128)", "rgba(255, 0, 128, 0.4)"],
+ ["rgb(128, 255, 0)", "rgba(128, 255, 0, 0.4)"],
+ ["rgb(0, 0, 128)", "rgba(0, 0, 128, 0.4)"]
+ ];
+
+ app_instance.get_data(w_instance.info.json_uri)
+ .then(function(result) {
+ var json_data = JSON.parse(result),
+ i = 0;
+
+ if (document.getElementById(w_instance.info.id) === null) {
+ return;
+ }
+
+ var content_container = document.getElementById(w_instance.info.id).children[1],
+ div_legend;
+
+ for (var key in json_data) {
+ if (json_data.hasOwnProperty(key)) {
+ if (w_instance.lines.length < i + 1) {
+ div_legend = document.createElement("div");
+ div_legend.id = w_instance.info.id + "_" + key;
+ div_legend.style.color = colors[i][0];
+ div_legend.classList.add("canvas_legend");
+ div_legend.appendChild(document.createTextNode (key));
+
+ content_container.appendChild(div_legend);
+
+ w_instance.lines.push(new TimeSeries());
+
+ w_instance.smoothie.addTimeSeries(
+ w_instance.lines[i],
+ {strokeStyle : colors[i][0],
+ fillStyle : colors[i][1],
+ lineWidth : 3}
+ );
+ } else {
+ div_legend = document.getElementById(
+ w_instance.info.id + "_" + key
+ );
+
+ if (div_legend === null) {
+ return;
+ }
+ }
+ div_legend.textContent = key + ": "
+ + json_data[key]
+ + w_instance.info.measure;
+ div_legend.classList.remove("slightly_visible");
+
+ w_instance.lines[i].append(new Date().getTime(),
+ Math.floor(json_data[key]));
+ document.getElementById(w_instance.info.id).classList.remove("slightly_visible");
+ i++;
+ }
+ }
+ })
+ .catch(function(result) {
+ console_log("update_canvas_data error: " + result);
+ });
+ };
+
+ /**
+ * Draw table widget
+ *
+ * @param {object} app_instance Application object instance
+ * @param {numeric} index Widget index
+ *
+ * @returns {undefined}
+ */
+ Widget.prototype.update_table_data = function (app_instance, index) {
+ var w_instance = app_instance.widgets[index];
+
+ app_instance.get_data(w_instance.info.json_uri)
+ .then(function(result) {
+ var json_data = JSON.parse(result);
+
+ if (document.getElementById(w_instance.info.id) !== null) {
+ w_instance.data = json_data;
+ w_instance.draw();
+ document.getElementById(w_instance.info.id).classList.remove("slightly_visible");
+ }
+ })
+ .catch(function(result) {
+ console_log("update_table_data error: " + result);
+ });
+ };
+
+ document.addEventListener("DOMContentLoaded", function () {
+ /**
+ * Initialize application object
+ */
+ var app = new Application();
+ });
+
+}());
diff --git a/js/smoothie.js b/js/smoothie.js
new file mode 100644
index 0000000..2d85f63
--- /dev/null
+++ b/js/smoothie.js
@@ -0,0 +1,1047 @@
+// MIT License:
+//
+// Copyright (c) 2010-2013, Joe Walnes
+// 2013-2018, Drew Noakes
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+/**
+ * Smoothie Charts - http://smoothiecharts.org/
+ * (c) 2010-2013, Joe Walnes
+ * 2013-2018, Drew Noakes
+ *
+ * v1.0: Main charting library, by Joe Walnes
+ * v1.1: Auto scaling of axis, by Neil Dunn
+ * v1.2: fps (frames per second) option, by Mathias Petterson
+ * v1.3: Fix for divide by zero, by Paul Nikitochkin
+ * v1.4: Set minimum, top-scale padding, remove timeseries, add optional timer to reset bounds, by Kelley Reynolds
+ * v1.5: Set default frames per second to 50... smoother.
+ * .start(), .stop() methods for conserving CPU, by Dmitry Vyal
+ * options.interpolation = 'bezier' or 'line', by Dmitry Vyal
+ * options.maxValue to fix scale, by Dmitry Vyal
+ * v1.6: minValue/maxValue will always get converted to floats, by Przemek Matylla
+ * v1.7: options.grid.fillStyle may be a transparent color, by Dmitry A. Shashkin
+ * Smooth rescaling, by Kostas Michalopoulos
+ * v1.8: Set max length to customize number of live points in the dataset with options.maxDataSetLength, by Krishna Narni
+ * v1.9: Display timestamps along the bottom, by Nick and Stev-io
+ * (https://groups.google.com/forum/?fromgroups#!topic/smoothie-charts/-Ywse8FCpKI%5B1-25%5D)
+ * Refactored by Krishna Narni, to support timestamp formatting function
+ * v1.10: Switch to requestAnimationFrame, removed the now obsoleted options.fps, by Gergely Imreh
+ * v1.11: options.grid.sharpLines option added, by @drewnoakes
+ * Addressed warning seen in Firefox when seriesOption.fillStyle undefined, by @drewnoakes
+ * v1.12: Support for horizontalLines added, by @drewnoakes
+ * Support for yRangeFunction callback added, by @drewnoakes
+ * v1.13: Fixed typo (#32), by @alnikitich
+ * v1.14: Timer cleared when last TimeSeries removed (#23), by @davidgaleano
+ * Fixed diagonal line on chart at start/end of data stream, by @drewnoakes
+ * v1.15: Support for npm package (#18), by @dominictarr
+ * Fixed broken removeTimeSeries function (#24) by @davidgaleano
+ * Minor performance and tidying, by @drewnoakes
+ * v1.16: Bug fix introduced in v1.14 relating to timer creation/clearance (#23), by @drewnoakes
+ * TimeSeries.append now deals with out-of-order timestamps, and can merge duplicates, by @zacwitte (#12)
+ * Documentation and some local variable renaming for clarity, by @drewnoakes
+ * v1.17: Allow control over font size (#10), by @drewnoakes
+ * Timestamp text won't overlap, by @drewnoakes
+ * v1.18: Allow control of max/min label precision, by @drewnoakes
+ * Added 'borderVisible' chart option, by @drewnoakes
+ * Allow drawing series with fill but no stroke (line), by @drewnoakes
+ * v1.19: Avoid unnecessary repaints, and fixed flicker in old browsers having multiple charts in document (#40), by @asbai
+ * v1.20: Add SmoothieChart.getTimeSeriesOptions and SmoothieChart.bringToFront functions, by @drewnoakes
+ * v1.21: Add 'step' interpolation mode, by @drewnoakes
+ * v1.22: Add support for different pixel ratios. Also add optional y limit formatters, by @copacetic
+ * v1.23: Fix bug introduced in v1.22 (#44), by @drewnoakes
+ * v1.24: Fix bug introduced in v1.23, re-adding parseFloat to y-axis formatter defaults, by @siggy_sf
+ * v1.25: Fix bug seen when adding a data point to TimeSeries which is older than the current data, by @Nking92
+ * Draw time labels on top of series, by @comolosabia
+ * Add TimeSeries.clear function, by @drewnoakes
+ * v1.26: Add support for resizing on high device pixel ratio screens, by @copacetic
+ * v1.27: Fix bug introduced in v1.26 for non whole number devicePixelRatio values, by @zmbush
+ * v1.28: Add 'minValueScale' option, by @megawac
+ * Fix 'labelPos' for different size of 'minValueString' 'maxValueString', by @henryn
+ * v1.29: Support responsive sizing, by @drewnoakes
+ * v1.29.1: Include types in package, and make property optional, by @TrentHouliston
+ * v1.30: Fix inverted logic in devicePixelRatio support, by @scanlime
+ * v1.31: Support tooltips, by @Sly1024 and @drewnoakes
+ * v1.32: Support frame rate limit, by @dpuyosa
+ * v1.33: Use Date static method instead of instance, by @nnnoel
+ * Fix bug with tooltips when multiple charts on a page, by @jpmbiz70
+ * v1.34: Add disabled option to TimeSeries, by @TechGuard (#91)
+ * Add nonRealtimeData option, by @annazhelt (#92, #93)
+ * Add showIntermediateLabels option, by @annazhelt (#94)
+ * Add displayDataFromPercentile option, by @annazhelt (#95)
+ * Fix bug when hiding tooltip element, by @ralphwetzel (#96)
+ * Support intermediate y-axis labels, by @beikeland (#99)
+ * v1.35: Fix issue with responsive mode at high DPI, by @drewnoakes (#101)
+ */
+
+;(function(exports) {
+
+ // Date.now polyfill
+ Date.now = Date.now || function() { return new Date().getTime(); };
+
+ var Util = {
+ extend: function() {
+ arguments[0] = arguments[0] || {};
+ for (var i = 1; i < arguments.length; i++)
+ {
+ for (var key in arguments[i])
+ {
+ if (arguments[i].hasOwnProperty(key))
+ {
+ if (typeof(arguments[i][key]) === 'object') {
+ if (arguments[i][key] instanceof Array) {
+ arguments[0][key] = arguments[i][key];
+ } else {
+ arguments[0][key] = Util.extend(arguments[0][key], arguments[i][key]);
+ }
+ } else {
+ arguments[0][key] = arguments[i][key];
+ }
+ }
+ }
+ }
+ return arguments[0];
+ },
+ binarySearch: function(data, value) {
+ var low = 0,
+ high = data.length;
+ while (low < high) {
+ var mid = (low + high) >> 1;
+ if (value < data[mid][0])
+ high = mid;
+ else
+ low = mid + 1;
+ }
+ return low;
+ }
+ };
+
+ /**
+ * Initialises a new TimeSeries with optional data options.
+ *
+ * Options are of the form (defaults shown):
+ *
+ *
+ * {
+ * resetBounds: true, // enables/disables automatic scaling of the y-axis
+ * resetBoundsInterval: 3000 // the period between scaling calculations, in millis
+ * }
+ *
+ *
+ * Presentation options for TimeSeries are specified as an argument to SmoothieChart.addTimeSeries.
+ *
+ * @constructor
+ */
+ function TimeSeries(options) {
+ this.options = Util.extend({}, TimeSeries.defaultOptions, options);
+ this.disabled = false;
+ this.clear();
+ }
+
+ TimeSeries.defaultOptions = {
+ resetBoundsInterval: 3000,
+ resetBounds: true
+ };
+
+ /**
+ * Clears all data and state from this TimeSeries object.
+ */
+ TimeSeries.prototype.clear = function() {
+ this.data = [];
+ this.maxValue = Number.NaN; // The maximum value ever seen in this TimeSeries.
+ this.minValue = Number.NaN; // The minimum value ever seen in this TimeSeries.
+ };
+
+ /**
+ * Recalculate the min/max values for this TimeSeries object.
+ *
+ * This causes the graph to scale itself in the y-axis.
+ */
+ TimeSeries.prototype.resetBounds = function() {
+ if (this.data.length) {
+ // Walk through all data points, finding the min/max value
+ this.maxValue = this.data[0][1];
+ this.minValue = this.data[0][1];
+ for (var i = 1; i < this.data.length; i++) {
+ var value = this.data[i][1];
+ if (value > this.maxValue) {
+ this.maxValue = value;
+ }
+ if (value < this.minValue) {
+ this.minValue = value;
+ }
+ }
+ } else {
+ // No data exists, so set min/max to NaN
+ this.maxValue = Number.NaN;
+ this.minValue = Number.NaN;
+ }
+ };
+
+ /**
+ * Adds a new data point to the TimeSeries, preserving chronological order.
+ *
+ * @param timestamp the position, in time, of this data point
+ * @param value the value of this data point
+ * @param sumRepeatedTimeStampValues if timestamp has an exact match in the series, this flag controls
+ * whether it is replaced, or the values summed (defaults to false.)
+ */
+ TimeSeries.prototype.append = function(timestamp, value, sumRepeatedTimeStampValues) {
+ // Rewind until we hit an older timestamp
+ var i = this.data.length - 1;
+ while (i >= 0 && this.data[i][0] > timestamp) {
+ i--;
+ }
+
+ if (i === -1) {
+ // This new item is the oldest data
+ this.data.splice(0, 0, [timestamp, value]);
+ } else if (this.data.length > 0 && this.data[i][0] === timestamp) {
+ // Update existing values in the array
+ if (sumRepeatedTimeStampValues) {
+ // Sum this value into the existing 'bucket'
+ this.data[i][1] += value;
+ value = this.data[i][1];
+ } else {
+ // Replace the previous value
+ this.data[i][1] = value;
+ }
+ } else if (i < this.data.length - 1) {
+ // Splice into the correct position to keep timestamps in order
+ this.data.splice(i + 1, 0, [timestamp, value]);
+ } else {
+ // Add to the end of the array
+ this.data.push([timestamp, value]);
+ }
+
+ this.maxValue = isNaN(this.maxValue) ? value : Math.max(this.maxValue, value);
+ this.minValue = isNaN(this.minValue) ? value : Math.min(this.minValue, value);
+ };
+
+ TimeSeries.prototype.dropOldData = function(oldestValidTime, maxDataSetLength) {
+ // We must always keep one expired data point as we need this to draw the
+ // line that comes into the chart from the left, but any points prior to that can be removed.
+ var removeCount = 0;
+ while (this.data.length - removeCount >= maxDataSetLength && this.data[removeCount + 1][0] < oldestValidTime) {
+ removeCount++;
+ }
+ if (removeCount !== 0) {
+ this.data.splice(0, removeCount);
+ }
+ };
+
+ /**
+ * Initialises a new SmoothieChart.
+ *
+ * Options are optional, and should be of the form below. Just specify the values you
+ * need and the rest will be given sensible defaults as shown:
+ *
+ *
+ * {
+ * minValue: undefined, // specify to clamp the lower y-axis to a given value
+ * maxValue: undefined, // specify to clamp the upper y-axis to a given value
+ * maxValueScale: 1, // allows proportional padding to be added above the chart. for 10% padding, specify 1.1.
+ * minValueScale: 1, // allows proportional padding to be added below the chart. for 10% padding, specify 1.1.
+ * yRangeFunction: undefined, // function({min: , max: }) { return {min: , max: }; }
+ * scaleSmoothing: 0.125, // controls the rate at which y-value zoom animation occurs
+ * millisPerPixel: 20, // sets the speed at which the chart pans by
+ * enableDpiScaling: true, // support rendering at different DPI depending on the device
+ * yMinFormatter: function(min, precision) { // callback function that formats the min y value label
+ * return parseFloat(min).toFixed(precision);
+ * },
+ * yMaxFormatter: function(max, precision) { // callback function that formats the max y value label
+ * return parseFloat(max).toFixed(precision);
+ * },
+ * yIntermediateFormatter: function(intermediate, precision) { // callback function that formats the intermediate y value labels
+ * return parseFloat(intermediate).toFixed(precision);
+ * },
+ * maxDataSetLength: 2,
+ * interpolation: 'bezier' // one of 'bezier', 'linear', or 'step'
+ * timestampFormatter: null, // optional function to format time stamps for bottom of chart
+ * // you may use SmoothieChart.timeFormatter, or your own: function(date) { return ''; }
+ * scrollBackwards: false, // reverse the scroll direction of the chart
+ * horizontalLines: [], // [ { value: 0, color: '#ffffff', lineWidth: 1 } ]
+ * grid:
+ * {
+ * fillStyle: '#000000', // the background colour of the chart
+ * lineWidth: 1, // the pixel width of grid lines
+ * strokeStyle: '#777777', // colour of grid lines
+ * millisPerLine: 1000, // distance between vertical grid lines
+ * sharpLines: false, // controls whether grid lines are 1px sharp, or softened
+ * verticalSections: 2, // number of vertical sections marked out by horizontal grid lines
+ * borderVisible: true // whether the grid lines trace the border of the chart or not
+ * },
+ * labels
+ * {
+ * disabled: false, // enables/disables labels showing the min/max values
+ * fillStyle: '#ffffff', // colour for text of labels,
+ * fontSize: 15,
+ * fontFamily: 'sans-serif',
+ * precision: 2,
+ * showIntermediateLabels: false, // shows intermediate labels between min and max values along y axis
+ * intermediateLabelSameAxis: true,
+ * },
+ * tooltip: false // show tooltip when mouse is over the chart
+ * tooltipLine: { // properties for a vertical line at the cursor position
+ * lineWidth: 1,
+ * strokeStyle: '#BBBBBB'
+ * },
+ * tooltipFormatter: SmoothieChart.tooltipFormatter, // formatter function for tooltip text
+ * nonRealtimeData: false, // use time of latest data as current time
+ * displayDataFromPercentile: 1, // display not latest data, but data from the given percentile
+ * // useful when trying to see old data saved by setting a high value for maxDataSetLength
+ * // should be a value between 0 and 1
+ * responsive: false, // whether the chart should adapt to the size of the canvas
+ * limitFPS: 0 // maximum frame rate the chart will render at, in FPS (zero means no limit)
+ * }
+ *
+ */
+ SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) {
+ this.seriesSet.push({timeSeries: timeSeries, options: Util.extend({}, SmoothieChart.defaultSeriesPresentationOptions, options)});
+ if (timeSeries.options.resetBounds && timeSeries.options.resetBoundsInterval > 0) {
+ timeSeries.resetBoundsTimerId = setInterval(
+ function() {
+ timeSeries.resetBounds();
+ },
+ timeSeries.options.resetBoundsInterval
+ );
+ }
+ };
+
+ /**
+ * Removes the specified TimeSeries from the chart.
+ */
+ SmoothieChart.prototype.removeTimeSeries = function(timeSeries) {
+ // Find the correct timeseries to remove, and remove it
+ var numSeries = this.seriesSet.length;
+ for (var i = 0; i < numSeries; i++) {
+ if (this.seriesSet[i].timeSeries === timeSeries) {
+ this.seriesSet.splice(i, 1);
+ break;
+ }
+ }
+ // If a timer was operating for that timeseries, remove it
+ if (timeSeries.resetBoundsTimerId) {
+ // Stop resetting the bounds, if we were
+ clearInterval(timeSeries.resetBoundsTimerId);
+ }
+ };
+
+ /**
+ * Gets render options for the specified TimeSeries.
+ *
+ * As you may use a single TimeSeries in multiple charts with different formatting in each usage,
+ * these settings are stored in the chart.
+ */
+ SmoothieChart.prototype.getTimeSeriesOptions = function(timeSeries) {
+ // Find the correct timeseries to remove, and remove it
+ var numSeries = this.seriesSet.length;
+ for (var i = 0; i < numSeries; i++) {
+ if (this.seriesSet[i].timeSeries === timeSeries) {
+ return this.seriesSet[i].options;
+ }
+ }
+ };
+
+ /**
+ * Brings the specified TimeSeries to the top of the chart. It will be rendered last.
+ */
+ SmoothieChart.prototype.bringToFront = function(timeSeries) {
+ // Find the correct timeseries to remove, and remove it
+ var numSeries = this.seriesSet.length;
+ for (var i = 0; i < numSeries; i++) {
+ if (this.seriesSet[i].timeSeries === timeSeries) {
+ var set = this.seriesSet.splice(i, 1);
+ this.seriesSet.push(set[0]);
+ break;
+ }
+ }
+ };
+
+ /**
+ * Instructs the SmoothieChart to start rendering to the provided canvas, with specified delay.
+ *
+ * @param canvas the target canvas element
+ * @param delayMillis an amount of time to wait before a data point is shown. This can prevent the end of the series
+ * from appearing on screen, with new values flashing into view, at the expense of some latency.
+ */
+ SmoothieChart.prototype.streamTo = function(canvas, delayMillis) {
+ this.canvas = canvas;
+ this.delay = delayMillis;
+ this.start();
+ };
+
+ SmoothieChart.prototype.getTooltipEl = function () {
+ // Create the tool tip element lazily
+ if (!this.tooltipEl) {
+ this.tooltipEl = document.createElement('div');
+ this.tooltipEl.className = 'smoothie-chart-tooltip';
+ this.tooltipEl.style.position = 'absolute';
+ this.tooltipEl.style.display = 'none';
+ document.body.appendChild(this.tooltipEl);
+ }
+ return this.tooltipEl;
+ };
+
+ SmoothieChart.prototype.updateTooltip = function () {
+ var el = this.getTooltipEl();
+
+ if (!this.mouseover || !this.options.tooltip) {
+ el.style.display = 'none';
+ return;
+ }
+
+ var time = this.lastChartTimestamp;
+
+ // x pixel to time
+ var t = this.options.scrollBackwards
+ ? time - this.mouseX * this.options.millisPerPixel
+ : time - (this.canvas.offsetWidth - this.mouseX) * this.options.millisPerPixel;
+
+ var data = [];
+
+ // For each data set...
+ for (var d = 0; d < this.seriesSet.length; d++) {
+ var timeSeries = this.seriesSet[d].timeSeries;
+ if (timeSeries.disabled) {
+ continue;
+ }
+
+ // find datapoint closest to time 't'
+ var closeIdx = Util.binarySearch(timeSeries.data, t);
+ if (closeIdx > 0 && closeIdx < timeSeries.data.length) {
+ data.push({ series: this.seriesSet[d], index: closeIdx, value: timeSeries.data[closeIdx][1] });
+ }
+ }
+
+ if (data.length) {
+ el.innerHTML = this.options.tooltipFormatter.call(this, t, data);
+ el.style.display = 'block';
+ } else {
+ el.style.display = 'none';
+ }
+ };
+
+ SmoothieChart.prototype.mousemove = function (evt) {
+ this.mouseover = true;
+ this.mouseX = evt.offsetX;
+ this.mouseY = evt.offsetY;
+ this.mousePageX = evt.pageX;
+ this.mousePageY = evt.pageY;
+
+ var el = this.getTooltipEl();
+ el.style.top = Math.round(this.mousePageY) + 'px';
+ el.style.left = Math.round(this.mousePageX) + 'px';
+ this.updateTooltip();
+ };
+
+ SmoothieChart.prototype.mouseout = function () {
+ this.mouseover = false;
+ this.mouseX = this.mouseY = -1;
+ if (this.tooltipEl)
+ this.tooltipEl.style.display = 'none';
+ };
+
+ /**
+ * Make sure the canvas has the optimal resolution for the device's pixel ratio.
+ */
+ SmoothieChart.prototype.resize = function () {
+ var dpr = !this.options.enableDpiScaling || !window ? 1 : window.devicePixelRatio,
+ width, height;
+ if (this.options.responsive) {
+ // Newer behaviour: Use the canvas's size in the layout, and set the internal
+ // resolution according to that size and the device pixel ratio (eg: high DPI)
+ width = this.canvas.offsetWidth;
+ height = this.canvas.offsetHeight;
+
+ if (width !== this.lastWidth) {
+ this.lastWidth = width;
+ this.canvas.setAttribute('width', (Math.floor(width * dpr)).toString());
+ this.canvas.getContext('2d').scale(dpr, dpr);
+ }
+ if (height !== this.lastHeight) {
+ this.lastHeight = height;
+ this.canvas.setAttribute('height', (Math.floor(height * dpr)).toString());
+ this.canvas.getContext('2d').scale(dpr, dpr);
+ }
+ } else if (dpr !== 1) {
+ // Older behaviour: use the canvas's inner dimensions and scale the element's size
+ // according to that size and the device pixel ratio (eg: high DPI)
+ width = parseInt(this.canvas.getAttribute('width'));
+ height = parseInt(this.canvas.getAttribute('height'));
+
+ if (!this.originalWidth || (Math.floor(this.originalWidth * dpr) !== width)) {
+ this.originalWidth = width;
+ this.canvas.setAttribute('width', (Math.floor(width * dpr)).toString());
+ this.canvas.style.width = width + 'px';
+ this.canvas.getContext('2d').scale(dpr, dpr);
+ }
+
+ if (!this.originalHeight || (Math.floor(this.originalHeight * dpr) !== height)) {
+ this.originalHeight = height;
+ this.canvas.setAttribute('height', (Math.floor(height * dpr)).toString());
+ this.canvas.style.height = height + 'px';
+ this.canvas.getContext('2d').scale(dpr, dpr);
+ }
+ }
+ };
+
+ /**
+ * Starts the animation of this chart.
+ */
+ SmoothieChart.prototype.start = function() {
+ if (this.frame) {
+ // We're already running, so just return
+ return;
+ }
+
+ this.canvas.addEventListener('mousemove', this.mousemove);
+ this.canvas.addEventListener('mouseout', this.mouseout);
+
+ // Renders a frame, and queues the next frame for later rendering
+ var animate = function() {
+ this.frame = SmoothieChart.AnimateCompatibility.requestAnimationFrame(function() {
+ if(this.options.nonRealtimeData){
+ var dateZero = new Date(0);
+ // find the data point with the latest timestamp
+ var maxTimeStamp = this.seriesSet.reduce(function(max, series){
+ var dataSet = series.timeSeries.data;
+ var indexToCheck = Math.round(this.options.displayDataFromPercentile * dataSet.length) - 1;
+ indexToCheck = indexToCheck >= 0 ? indexToCheck : 0;
+ indexToCheck = indexToCheck <= dataSet.length -1 ? indexToCheck : dataSet.length -1;
+ if(dataSet && dataSet.length > 0)
+ {
+ // timestamp corresponds to element 0 of the data point
+ var lastDataTimeStamp = dataSet[indexToCheck][0];
+ max = max > lastDataTimeStamp ? max : lastDataTimeStamp;
+ }
+ return max;
+ }.bind(this), dateZero);
+ // use the max timestamp as current time
+ this.render(this.canvas, maxTimeStamp > dateZero ? maxTimeStamp : null);
+ } else {
+ this.render();
+ }
+ animate();
+ }.bind(this));
+ }.bind(this);
+
+ animate();
+ };
+
+ /**
+ * Stops the animation of this chart.
+ */
+ SmoothieChart.prototype.stop = function() {
+ if (this.frame) {
+ SmoothieChart.AnimateCompatibility.cancelAnimationFrame(this.frame);
+ delete this.frame;
+ this.canvas.removeEventListener('mousemove', this.mousemove);
+ this.canvas.removeEventListener('mouseout', this.mouseout);
+ }
+ };
+
+ SmoothieChart.prototype.updateValueRange = function() {
+ // Calculate the current scale of the chart, from all time series.
+ var chartOptions = this.options,
+ chartMaxValue = Number.NaN,
+ chartMinValue = Number.NaN;
+
+ for (var d = 0; d < this.seriesSet.length; d++) {
+ // TODO(ndunn): We could calculate / track these values as they stream in.
+ var timeSeries = this.seriesSet[d].timeSeries;
+ if (timeSeries.disabled) {
+ continue;
+ }
+
+ if (!isNaN(timeSeries.maxValue)) {
+ chartMaxValue = !isNaN(chartMaxValue) ? Math.max(chartMaxValue, timeSeries.maxValue) : timeSeries.maxValue;
+ }
+
+ if (!isNaN(timeSeries.minValue)) {
+ chartMinValue = !isNaN(chartMinValue) ? Math.min(chartMinValue, timeSeries.minValue) : timeSeries.minValue;
+ }
+ }
+
+ // Scale the chartMaxValue to add padding at the top if required
+ if (chartOptions.maxValue != null) {
+ chartMaxValue = chartOptions.maxValue;
+ } else {
+ chartMaxValue *= chartOptions.maxValueScale;
+ }
+
+ // Set the minimum if we've specified one
+ if (chartOptions.minValue != null) {
+ chartMinValue = chartOptions.minValue;
+ } else {
+ chartMinValue -= Math.abs(chartMinValue * chartOptions.minValueScale - chartMinValue);
+ }
+
+ // If a custom range function is set, call it
+ if (this.options.yRangeFunction) {
+ var range = this.options.yRangeFunction({min: chartMinValue, max: chartMaxValue});
+ chartMinValue = range.min;
+ chartMaxValue = range.max;
+ }
+
+ if (!isNaN(chartMaxValue) && !isNaN(chartMinValue)) {
+ var targetValueRange = chartMaxValue - chartMinValue;
+ var valueRangeDiff = (targetValueRange - this.currentValueRange);
+ var minValueDiff = (chartMinValue - this.currentVisMinValue);
+ this.isAnimatingScale = Math.abs(valueRangeDiff) > 0.1 || Math.abs(minValueDiff) > 0.1;
+ this.currentValueRange += chartOptions.scaleSmoothing * valueRangeDiff;
+ this.currentVisMinValue += chartOptions.scaleSmoothing * minValueDiff;
+ }
+
+ this.valueRange = { min: chartMinValue, max: chartMaxValue };
+ };
+
+ SmoothieChart.prototype.render = function(canvas, time) {
+ var nowMillis = Date.now();
+
+ // Respect any frame rate limit.
+ if (this.options.limitFPS > 0 && nowMillis - this.lastRenderTimeMillis < (1000/this.options.limitFPS))
+ return;
+
+ if (!this.isAnimatingScale) {
+ // We're not animating. We can use the last render time and the scroll speed to work out whether
+ // we actually need to paint anything yet. If not, we can return immediately.
+
+ // Render at least every 1/6th of a second. The canvas may be resized, which there is
+ // no reliable way to detect.
+ var maxIdleMillis = Math.min(1000/6, this.options.millisPerPixel);
+
+ if (nowMillis - this.lastRenderTimeMillis < maxIdleMillis) {
+ return;
+ }
+ }
+
+ this.resize();
+ this.updateTooltip();
+
+ this.lastRenderTimeMillis = nowMillis;
+
+ canvas = canvas || this.canvas;
+ time = time || nowMillis - (this.delay || 0);
+
+ // Round time down to pixel granularity, so motion appears smoother.
+ time -= time % this.options.millisPerPixel;
+
+ this.lastChartTimestamp = time;
+
+ var context = canvas.getContext('2d'),
+ chartOptions = this.options,
+ dimensions = { top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight },
+ // Calculate the threshold time for the oldest data points.
+ oldestValidTime = time - (dimensions.width * chartOptions.millisPerPixel),
+ valueToYPixel = function(value) {
+ var offset = value - this.currentVisMinValue;
+ return this.currentValueRange === 0
+ ? dimensions.height
+ : dimensions.height - (Math.round((offset / this.currentValueRange) * dimensions.height));
+ }.bind(this),
+ timeToXPixel = function(t) {
+ if(chartOptions.scrollBackwards) {
+ return Math.round((time - t) / chartOptions.millisPerPixel);
+ }
+ return Math.round(dimensions.width - ((time - t) / chartOptions.millisPerPixel));
+ };
+
+ this.updateValueRange();
+
+ context.font = chartOptions.labels.fontSize + 'px ' + chartOptions.labels.fontFamily;
+
+ // Save the state of the canvas context, any transformations applied in this method
+ // will get removed from the stack at the end of this method when .restore() is called.
+ context.save();
+
+ // Move the origin.
+ context.translate(dimensions.left, dimensions.top);
+
+ // Create a clipped rectangle - anything we draw will be constrained to this rectangle.
+ // This prevents the occasional pixels from curves near the edges overrunning and creating
+ // screen cheese (that phrase should need no explanation).
+ context.beginPath();
+ context.rect(0, 0, dimensions.width, dimensions.height);
+ context.clip();
+
+ // Clear the working area.
+ context.save();
+ context.fillStyle = chartOptions.grid.fillStyle;
+ context.clearRect(0, 0, dimensions.width, dimensions.height);
+ context.fillRect(0, 0, dimensions.width, dimensions.height);
+ context.restore();
+
+ // Grid lines...
+ context.save();
+ context.lineWidth = chartOptions.grid.lineWidth;
+ context.strokeStyle = chartOptions.grid.strokeStyle;
+ // Vertical (time) dividers.
+ if (chartOptions.grid.millisPerLine > 0) {
+ context.beginPath();
+ for (var t = time - (time % chartOptions.grid.millisPerLine);
+ t >= oldestValidTime;
+ t -= chartOptions.grid.millisPerLine) {
+ var gx = timeToXPixel(t);
+ if (chartOptions.grid.sharpLines) {
+ gx -= 0.5;
+ }
+ context.moveTo(gx, 0);
+ context.lineTo(gx, dimensions.height);
+ }
+ context.stroke();
+ context.closePath();
+ }
+
+ // Horizontal (value) dividers.
+ for (var v = 1; v < chartOptions.grid.verticalSections; v++) {
+ var gy = Math.round(v * dimensions.height / chartOptions.grid.verticalSections);
+ if (chartOptions.grid.sharpLines) {
+ gy -= 0.5;
+ }
+ context.beginPath();
+ context.moveTo(0, gy);
+ context.lineTo(dimensions.width, gy);
+ context.stroke();
+ context.closePath();
+ }
+ // Bounding rectangle.
+ if (chartOptions.grid.borderVisible) {
+ context.beginPath();
+ context.strokeRect(0, 0, dimensions.width, dimensions.height);
+ context.closePath();
+ }
+ context.restore();
+
+ // Draw any horizontal lines...
+ if (chartOptions.horizontalLines && chartOptions.horizontalLines.length) {
+ for (var hl = 0; hl < chartOptions.horizontalLines.length; hl++) {
+ var line = chartOptions.horizontalLines[hl],
+ hly = Math.round(valueToYPixel(line.value)) - 0.5;
+ context.strokeStyle = line.color || '#ffffff';
+ context.lineWidth = line.lineWidth || 1;
+ context.beginPath();
+ context.moveTo(0, hly);
+ context.lineTo(dimensions.width, hly);
+ context.stroke();
+ context.closePath();
+ }
+ }
+
+ // For each data set...
+ for (var d = 0; d < this.seriesSet.length; d++) {
+ context.save();
+ var timeSeries = this.seriesSet[d].timeSeries;
+ if (timeSeries.disabled) {
+ continue;
+ }
+
+ var dataSet = timeSeries.data,
+ seriesOptions = this.seriesSet[d].options;
+
+ // Delete old data that's moved off the left of the chart.
+ timeSeries.dropOldData(oldestValidTime, chartOptions.maxDataSetLength);
+
+ // Set style for this dataSet.
+ context.lineWidth = seriesOptions.lineWidth;
+ context.strokeStyle = seriesOptions.strokeStyle;
+ // Draw the line...
+ context.beginPath();
+ // Retain lastX, lastY for calculating the control points of bezier curves.
+ var firstX = 0, lastX = 0, lastY = 0;
+ for (var i = 0; i < dataSet.length && dataSet.length !== 1; i++) {
+ var x = timeToXPixel(dataSet[i][0]),
+ y = valueToYPixel(dataSet[i][1]);
+
+ if (i === 0) {
+ firstX = x;
+ context.moveTo(x, y);
+ } else {
+ switch (chartOptions.interpolation) {
+ case "linear":
+ case "line": {
+ context.lineTo(x,y);
+ break;
+ }
+ case "bezier":
+ default: {
+ // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves
+ //
+ // Assuming A was the last point in the line plotted and B is the new point,
+ // we draw a curve with control points P and Q as below.
+ //
+ // A---P
+ // |
+ // |
+ // |
+ // Q---B
+ //
+ // Importantly, A and P are at the same y coordinate, as are B and Q. This is
+ // so adjacent curves appear to flow as one.
+ //
+ context.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop
+ Math.round((lastX + x) / 2), lastY, // controlPoint1 (P)
+ Math.round((lastX + x)) / 2, y, // controlPoint2 (Q)
+ x, y); // endPoint (B)
+ break;
+ }
+ case "step": {
+ context.lineTo(x,lastY);
+ context.lineTo(x,y);
+ break;
+ }
+ }
+ }
+
+ lastX = x; lastY = y;
+ }
+
+ if (dataSet.length > 1) {
+ if (seriesOptions.fillStyle) {
+ // Close up the fill region.
+ context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY);
+ context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1);
+ context.lineTo(firstX, dimensions.height + seriesOptions.lineWidth);
+ context.fillStyle = seriesOptions.fillStyle;
+ context.fill();
+ }
+
+ if (seriesOptions.strokeStyle && seriesOptions.strokeStyle !== 'none') {
+ context.stroke();
+ }
+ context.closePath();
+ }
+ context.restore();
+ }
+
+ if (chartOptions.tooltip && this.mouseX >= 0) {
+ // Draw vertical bar to show tooltip position
+ context.lineWidth = chartOptions.tooltipLine.lineWidth;
+ context.strokeStyle = chartOptions.tooltipLine.strokeStyle;
+ context.beginPath();
+ context.moveTo(this.mouseX, 0);
+ context.lineTo(this.mouseX, dimensions.height);
+ context.closePath();
+ context.stroke();
+ this.updateTooltip();
+ }
+
+ // Draw the axis values on the chart.
+ if (!chartOptions.labels.disabled && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)) {
+ var maxValueString = chartOptions.yMaxFormatter(this.valueRange.max, chartOptions.labels.precision),
+ minValueString = chartOptions.yMinFormatter(this.valueRange.min, chartOptions.labels.precision),
+ maxLabelPos = chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(maxValueString).width - 2,
+ minLabelPos = chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(minValueString).width - 2;
+ context.fillStyle = chartOptions.labels.fillStyle;
+ context.fillText(maxValueString, maxLabelPos, chartOptions.labels.fontSize);
+ context.fillText(minValueString, minLabelPos, dimensions.height - 2);
+ }
+
+ // Display intermediate y axis labels along y-axis to the left of the chart
+ if ( chartOptions.labels.showIntermediateLabels
+ && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)
+ && chartOptions.grid.verticalSections > 0) {
+ // show a label above every vertical section divider
+ var step = (this.valueRange.max - this.valueRange.min) / chartOptions.grid.verticalSections;
+ var stepPixels = dimensions.height / chartOptions.grid.verticalSections;
+ for (var v = 1; v < chartOptions.grid.verticalSections; v++) {
+ var gy = dimensions.height - Math.round(v * stepPixels);
+ if (chartOptions.grid.sharpLines) {
+ gy -= 0.5;
+ }
+ var yValue = chartOptions.yIntermediateFormatter(this.valueRange.min + (v * step), chartOptions.labels.precision);
+ //left of right axis?
+ intermediateLabelPos =
+ chartOptions.labels.intermediateLabelSameAxis
+ ? (chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(yValue).width - 2)
+ : (chartOptions.scrollBackwards ? dimensions.width - context.measureText(yValue).width - 2 : 0);
+
+ context.fillText(yValue, intermediateLabelPos, gy - chartOptions.grid.lineWidth);
+ }
+ }
+
+ // Display timestamps along x-axis at the bottom of the chart.
+ if (chartOptions.timestampFormatter && chartOptions.grid.millisPerLine > 0) {
+ var textUntilX = chartOptions.scrollBackwards
+ ? context.measureText(minValueString).width
+ : dimensions.width - context.measureText(minValueString).width + 4;
+ for (var t = time - (time % chartOptions.grid.millisPerLine);
+ t >= oldestValidTime;
+ t -= chartOptions.grid.millisPerLine) {
+ var gx = timeToXPixel(t);
+ // Only draw the timestamp if it won't overlap with the previously drawn one.
+ if ((!chartOptions.scrollBackwards && gx < textUntilX) || (chartOptions.scrollBackwards && gx > textUntilX)) {
+ // Formats the timestamp based on user specified formatting function
+ // SmoothieChart.timeFormatter function above is one such formatting option
+ var tx = new Date(t),
+ ts = chartOptions.timestampFormatter(tx),
+ tsWidth = context.measureText(ts).width;
+
+ textUntilX = chartOptions.scrollBackwards
+ ? gx + tsWidth + 2
+ : gx - tsWidth - 2;
+
+ context.fillStyle = chartOptions.labels.fillStyle;
+ if(chartOptions.scrollBackwards) {
+ context.fillText(ts, gx, dimensions.height - 2);
+ } else {
+ context.fillText(ts, gx - tsWidth, dimensions.height - 2);
+ }
+ }
+ }
+ }
+
+ context.restore(); // See .save() above.
+ };
+
+ // Sample timestamp formatting function
+ SmoothieChart.timeFormatter = function(date) {
+ function pad2(number) { return (number < 10 ? '0' : '') + number }
+ return pad2(date.getHours()) + ':' + pad2(date.getMinutes()) + ':' + pad2(date.getSeconds());
+ };
+
+ exports.TimeSeries = TimeSeries;
+ exports.SmoothieChart = SmoothieChart;
+
+})(typeof exports === 'undefined' ? this : exports);
diff --git a/system/action/index.php b/system/action/index.php
new file mode 100644
index 0000000..557c474
--- /dev/null
+++ b/system/action/index.php
@@ -0,0 +1,62 @@
+translate('reboot_in_progress') . ' ';
+ echo shell_exec('sudo shutdown -r');
+ } else {
+ echo $lang->translate('reboot_prohibited') . ' ';
+ }
+} else if ($action === 'shutdown') {
+ if (SHUTDOWN_ENABLED) {
+ echo $lang->translate('shutdown_in_progress') . ' ';
+ echo shell_exec('sudo halt -p');
+ } else {
+ echo $lang->translate('shutdown_prohibited') . ' ';
+ }
+} else if ($action === 'setup') {
+ $def_lang = filter_input(INPUT_GET, 'default_language', FILTER_SANITIZE_STRING);
+ $dim_on_create = filter_input(INPUT_GET, 'dim_on_create', FILTER_SANITIZE_NUMBER_INT) == 1;
+ $chk_f_rights = filter_input(INPUT_GET, 'chk_files_rights', FILTER_SANITIZE_NUMBER_INT) == 1;
+ $max_hdd_temp = filter_input(INPUT_GET, 'max_hdd_temp', FILTER_SANITIZE_NUMBER_INT);
+ $chk_hdd_temp_int = filter_input(INPUT_GET, 'chk_temp_interval', FILTER_SANITIZE_NUMBER_INT);
+ $chk_smart_int = filter_input(INPUT_GET, 'chk_smart_interval', FILTER_SANITIZE_NUMBER_INT);
+ $chk_users_online_int = filter_input(INPUT_GET, 'chk_users_online_interval', FILTER_SANITIZE_NUMBER_INT);
+
+ $user_cfg->set_value('lang', $def_lang);
+ $user_cfg->set_value('dim_on_create', $dim_on_create);
+ $user_cfg->set_value('check_files_rights', $chk_f_rights);
+ $user_cfg->set_value('max_hdd_temp', $max_hdd_temp);
+ $user_cfg->set_value('check_hdd_temp_interval', $chk_hdd_temp_int);
+ $user_cfg->set_value('check_smart_interval', $chk_smart_int);
+ $user_cfg->set_value('check_users_online_interval', $chk_users_online_int);
+
+ $uc_saved = $user_cfg->save();
+
+ /* Apps list */
+ $enabled_apps = array();
+ foreach ($_GET["app_caption"] as $item => $val) {
+ if (isset($_GET["app_enabled"][$item]) && (bool)$_GET["app_enabled"][$item]) {
+ $app_cap = filter_var($_GET["app_caption"][$item], FILTER_SANITIZE_STRING);
+ $app_name = filter_var($_GET["app_name"][$item], FILTER_SANITIZE_STRING);
+ $enabled_apps[$app_cap] = $app_name;
+ }
+ }
+ $app = new \CAI\CAICP\Applications(CP_ROOT_REL);
+ $app->set($enabled_apps);
+ $apps_saved = $app->save();
+
+ if ($uc_saved && $apps_saved) {
+ echo 'true';
+ } else {
+ echo 'false';
+ }
+}
diff --git a/system/apps/apps.json b/system/apps/apps.json
new file mode 100644
index 0000000..af0830d
--- /dev/null
+++ b/system/apps/apps.json
@@ -0,0 +1,11 @@
+{
+ "Plex" : "plex",
+ "Transmission" : "transmission",
+ "Resilio sync" : "rslsync",
+ "NextCloud" : "nextcloud",
+ "OwnCloud" : "owncloud",
+ "Files downloader" : "cai_downloader",
+ "OpenFire" : "openfire",
+ "phpMyAdmin" : "phpmyadmin",
+ "phpPgAdmin" : "phppgadmin"
+}
diff --git a/system/apps/cai_downloader/app.json b/system/apps/cai_downloader/app.json
new file mode 100644
index 0000000..d39eb2c
--- /dev/null
+++ b/system/apps/cai_downloader/app.json
@@ -0,0 +1,16 @@
+{
+ "app_name" : "app_cai_downloader",
+ "caption" : "Files downloader",
+ "version" : "1.0.20170705",
+ "author" : "Alexander I. Chebykin",
+ "http_uri" : "system/apps/cai_downloader/",
+ "use_http" : true,
+ "https_uri" : "system/apps/cai_downloader/",
+ "use_https" : true,
+ "frame_support" : true,
+ "require_php" : true,
+ "require" : [
+ {"data" : "php"},
+ {"data" : "curl"}
+ ]
+}
diff --git a/system/apps/cai_downloader/css/downloader.css b/system/apps/cai_downloader/css/downloader.css
new file mode 100644
index 0000000..5943c9f
--- /dev/null
+++ b/system/apps/cai_downloader/css/downloader.css
@@ -0,0 +1,32 @@
+.full_width {width: 100%}
+.hidden {opacity: 0;}
+.td_label {width: 100px;}
+.del_rec {
+ position: absolute;
+ right: 15px;
+ cursor: pointer;
+}
+
+#in_submit {margin: 10px auto;}
+#div_download,
+#div_log,
+.div_log_rec {transition: all 0.5s ease-out 0.1s;}
+
+.div_log_rec span {margin-right: 20px;}
+
+fieldset {
+ border-width: 1px;
+ border-color: #ddd;
+}
+
+hr {
+ margin: 5px 0;
+ padding: 0;
+ border: 0;
+ height: 1px;
+ background-image: -webkit-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0));
+ background-image: -moz-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0));
+ background-image: -ms-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0));
+ background-image: -o-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0));
+ background: linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0, 0));
+}
\ No newline at end of file
diff --git a/system/apps/cai_downloader/exec/index.php b/system/apps/cai_downloader/exec/index.php
new file mode 100644
index 0000000..26133a5
--- /dev/null
+++ b/system/apps/cai_downloader/exec/index.php
@@ -0,0 +1,69 @@
+
+ * @copyright : Copyright (c) 2016-2017 Alexander I. Chebykin
+ * @version : 1.0
+ * @build date : 2017-07-17
+ * @license : MIT
+ * @link : https://github.com/CAI79/CAI-CP
+ ******************************************************************/
+
+header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
+header('Cache-Control: post-check=0, pre-check=0', false);
+header('Pragma: no-cache');
+
+require_once('../../../settings.php');
+
+/* Places to store files by type */
+$ParentDir = array('.gif' => 'images/_animations/',
+ '.jpg' => 'images/', '.jpeg' => 'images/', '.png' => 'images/',
+ '.tif' => 'images/', '.tiff' => 'images/',
+ '.avi' => 'video/', '.flv' => 'video/', '.mkv' => 'video/',
+ '.mp4' => 'video/', '.mpeg' => 'video/', '.mpg' => 'video/',
+ '.exe' => 'soft/', '.msi' => 'soft/',
+ '.7z' => 'archives/', '.iso' => 'archives/', '.rar' => 'archives/',
+ '.tar' => 'archives/', '.zip' => 'archives/');
+/* Filter parameters */
+$Dir = str_replace(array('.', '/', '~', '\\', ' ', '%'),
+ '_',
+ filter_input(INPUT_POST, 'dir', FILTER_SANITIZE_STRING)
+ );
+$Overwrite = (int)filter_input(INPUT_POST, 'overwrite', FILTER_SANITIZE_NUMBER_INT);
+
+$URI = filter_input(INPUT_POST, 'uri', FILTER_SANITIZE_URL);
+
+$SubstrPos = 0;
+$PDir = '';
+foreach ($ParentDir as $Key => $Val) {
+ if (mb_stripos($URI, $Key) !== false){
+ $SPos = mb_stripos($URI, $Key);
+ if ($SubstrPos == 0) {
+ $SubstrPos = $SPos;
+ $PDir = $Val;
+ } else {
+ if ($SubstrPos < $Pos && $Pos > 0) {
+ $SubstrPos = $Pos;
+ $PDir = $Val;
+ }
+ }
+ }
+}
+
+if ($Overwrite !== 1 && file_exists(str_replace('\ ', ' ', FILE_DOWNLOAD_DIR) . $PDir . $Dir . '/')) {
+ $i = 1;
+ while (file_exists(str_replace('\ ', ' ', FILE_DOWNLOAD_DIR) . $PDir . $Dir . '/' . $i)) {
+ $i++;
+ }
+ $Dir .= '/' . $i;
+}
+
+echo exec(sprintf('bash -c "%s > /dev/null 2>&1 &"',
+ sprintf("./scripts/app.sh %s %s",
+ FILE_DOWNLOAD_DIR . $PDir . $Dir,
+ $URI)
+ )
+ );
+echo '[langDownloadStarted]';
diff --git a/system/apps/cai_downloader/exec/scripts/app.sh b/system/apps/cai_downloader/exec/scripts/app.sh
new file mode 100644
index 0000000..e6fcb10
--- /dev/null
+++ b/system/apps/cai_downloader/exec/scripts/app.sh
@@ -0,0 +1,3 @@
+#! /bin/bash
+mkdir -p "$1" && cd "$1" && curl -L -S -O $2 >> download.log 2>&1 &
+# mkdir -p "$1" && cd "$1" && curl -L -O $2 > /dev/null 2>&1 &
\ No newline at end of file
diff --git a/system/apps/cai_downloader/icon.png b/system/apps/cai_downloader/icon.png
new file mode 100644
index 0000000..09bf9ad
Binary files /dev/null and b/system/apps/cai_downloader/icon.png differ
diff --git a/system/apps/cai_downloader/index.html b/system/apps/cai_downloader/index.html
new file mode 100644
index 0000000..a5239e0
--- /dev/null
+++ b/system/apps/cai_downloader/index.html
@@ -0,0 +1,29 @@
+
+
+
+ Files downloader app for CAI CP
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/system/apps/cai_downloader/js/downloader.js b/system/apps/cai_downloader/js/downloader.js
new file mode 100644
index 0000000..678926f
--- /dev/null
+++ b/system/apps/cai_downloader/js/downloader.js
@@ -0,0 +1,206 @@
+// MIT License:
+//
+// Copyright (c) 2016-2017, Alexander I. Chebykin
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+/**
+ * CAI CP v.1
+ *
+ * @module : Downloader app
+ * @author : Alexander I. Chebykin
+ * @copyright : Copyright (c) 2016-2017 Alexander I. Chebykin
+ * @version : 1.0
+ * @build date : 2017-06-05
+ * @license : MIT
+ * @link : https://github.com/CAI79/CAI-CP
+ ******************************************************************/
+
+(function () {
+ 'use strict';
+
+ /**
+ * Return GET parameter from parent page
+ *
+ * @param {string} key Key name
+ *
+ * returns {string}
+ */
+ function $_PARENT_GET(key) {
+// var s = window.location.search;
+ var s = (window.location !== window.parent.location)
+ ? document.referrer
+ : document.location.href;
+
+ s = s.match(new RegExp(key + '=([^&=]+)'));
+
+ return s ? s[1] : false;
+ }
+
+ /**
+ * Return GET parameter
+ *
+ * @param {string} key Key name
+ *
+ * returns {string}
+ */
+ function $_GET(key) {
+// var s = window.location.search;
+ var s = document.referrer;
+
+ s = s.match(new RegExp(key + '=([^&=]+)'));
+
+ return s ? s[1] : false;
+ }
+
+ /**
+ * Download system constructor
+ *
+ * @returns {undefined}
+ */
+ function DownloadSystem() {
+ this.locales = 'en ru';
+ this.lang = {};
+ }
+
+ /**
+ * Download system initialization
+ *
+ * @param {string} lang Language code. Ex: en, ru etc.
+ *
+ * @returns {undefined}
+ */
+ DownloadSystem.prototype.init = function (lang) {
+ var dl_instance = this,
+ json_file = 'locale/',
+ request = new XMLHttpRequest();
+
+ if (this.check_locale(lang)) {
+ json_file += lang + '.json';
+ } else {
+ json_file += 'en.json';
+ }
+
+ if (document.getElementById('lang') !== null) {
+ document.getElementById('lang').value = lang;
+ }
+
+ if ($_GET('overwrite') && (document.getElementById('overwrite') !== null)) {
+ document.getElementById('overwrite').checked = true;
+ }
+
+ request.open('GET', json_file);
+ request.send();
+
+ request.onreadystatechange = function () {
+ if (this.readyState === 4) {
+ if (this.status === 200) {
+ dl_instance.lang = JSON.parse(this.responseText);
+
+ document.getElementById('lbl_uri').textContent = dl_instance.lang.uri + ': ';
+ document.getElementById('lbl_dir').textContent = dl_instance.lang.dir + ': ';
+ document.getElementById('lbl_overwrite').textContent = dl_instance.lang.overwrite;
+ document.getElementById('lgn_log').textContent = dl_instance.lang.log;
+ document.getElementById('in_submit').value = dl_instance.lang.go;
+ document.getElementById('div_download').classList.toggle('hidden');
+ document.getElementById('div_log').classList.toggle('hidden');
+ }
+ }
+ };
+ };
+
+ /**
+ * Check locale support
+ *
+ * @param {string} locale Locale code to check. Ex: en, ru etc.
+ *
+ * @returns {Boolean}
+ */
+ DownloadSystem.prototype.check_locale = function (locale) {
+ return this.locales.indexOf(locale.toLowerCase()) !== -1;
+ };
+
+ document.addEventListener('DOMContentLoaded', function () {
+ /**
+ * Initialize download object
+ */
+ var download = new DownloadSystem();
+
+ if ($_GET('lang')) {
+ download.init($_GET('lang'));
+ } else if ($_PARENT_GET('lang')) {
+ download.init($_PARENT_GET('lang'));
+ } else {
+ download.init('en');
+ }
+
+ document.getElementById('frm_submit').onsubmit = function () {
+ var xhr = new XMLHttpRequest(),
+ msg,
+ cur_date = new Date();
+
+ xhr.onload = function () {
+ var log_rec = document.createElement('div'),
+ log_span = document.createElement('span'),
+ img_del = document.createElement('img'),
+ hr = document.createElement('hr');
+
+ log_rec.classList.add('div_log_rec');
+ log_rec.classList.add('hidden');
+
+ img_del.classList.add('del_rec');
+ img_del.src = '../../../gfx/buttons/delete.png';
+ img_del.title = download.lang.del_log_rec;
+
+ img_del.addEventListener('click', function () {
+ this.parentNode.parentNode.removeChild(this.parentNode);
+ });
+
+ log_rec.appendChild(log_span);
+ log_rec.appendChild(img_del);
+ log_rec.appendChild(hr);
+
+// document.getElementById('div_log_recs').appendChild(log_rec);
+ document.getElementById('div_log_recs').insertBefore(log_rec,
+ document.getElementById('div_log_recs').childNodes[0]);
+
+ if (xhr.responseText === '[langDownloadStarted]') {
+ msg = download.lang.download_started;
+ } else {
+ msg = xhr.responseText;
+ }
+
+ log_span.appendChild(document.createTextNode(
+ cur_date.toLocaleString() + ': ' +
+ document.getElementById('dir').value + ' (' +
+ document.getElementById('uri').value + '): ' +
+ msg
+ )
+ );
+
+ log_rec.classList.toggle('hidden');
+ };
+
+ xhr.open(this.method, this.action, true);
+ xhr.send(new FormData(this));
+
+ return false;
+ };
+ });
+}());
diff --git a/system/apps/cai_downloader/locale/en.json b/system/apps/cai_downloader/locale/en.json
new file mode 100644
index 0000000..8f136de
--- /dev/null
+++ b/system/apps/cai_downloader/locale/en.json
@@ -0,0 +1,9 @@
+{
+ "uri" : "URI",
+ "dir" : "Directory",
+ "overwrite" : "Overwrite",
+ "go" : "Go!",
+ "log" : "Log",
+ "del_log_rec" : "Delete log record",
+ "download_started" : "Download started"
+}
diff --git a/system/apps/cai_downloader/locale/ru.json b/system/apps/cai_downloader/locale/ru.json
new file mode 100644
index 0000000..7d37080
--- /dev/null
+++ b/system/apps/cai_downloader/locale/ru.json
@@ -0,0 +1,9 @@
+{
+ "uri" : "URI",
+ "dir" : "Директория",
+ "overwrite" : "Перезаписать",
+ "go" : "Поехали!",
+ "log" : "Журнал",
+ "del_log_rec" : "Удалить запись журнала",
+ "download_started" : "Загрузка начата"
+}
diff --git a/system/apps/cai_downloader/readme/en.txt b/system/apps/cai_downloader/readme/en.txt
new file mode 100644
index 0000000..1f6fd13
--- /dev/null
+++ b/system/apps/cai_downloader/readme/en.txt
@@ -0,0 +1,4 @@
+Settings
+========
+Set $DataDir variable value to download dir in exec/index.php file.
+Grant write access for this directory to web server.
diff --git a/system/apps/cai_downloader/readme/ru.txt b/system/apps/cai_downloader/readme/ru.txt
new file mode 100644
index 0000000..ecce632
--- /dev/null
+++ b/system/apps/cai_downloader/readme/ru.txt
@@ -0,0 +1,4 @@
+Настройка
+=========
+В файле exec/index.php задайте каталог для загрузки файлов в переменной $DataDir.
+Права на каталог должны позволять запись веб-серверу.
diff --git a/system/apps/index.php b/system/apps/index.php
new file mode 100644
index 0000000..6e51d82
--- /dev/null
+++ b/system/apps/index.php
@@ -0,0 +1,16 @@
+get_avail_json();
+} elseif ($action === 'get_enabled_apps') {
+ $apps = new \CAI\CAICP\Applications(CP_ROOT_REL);
+ echo $apps->get_enabled_json();
+}
diff --git a/system/apps/nextcloud/app.json b/system/apps/nextcloud/app.json
new file mode 100644
index 0000000..80fa949
--- /dev/null
+++ b/system/apps/nextcloud/app.json
@@ -0,0 +1,12 @@
+{
+ "app_name" : "app_nextcloud",
+ "caption" : "NextCloud",
+ "http_uri" : "",
+ "use_http" : false,
+ "https_uri" : "https://[server_name]/",
+ "use_https" : true,
+ "frame_support" : false,
+ "require" : [
+ {"data" : "php"}
+ ]
+}
diff --git a/system/apps/nextcloud/icon.png b/system/apps/nextcloud/icon.png
new file mode 100644
index 0000000..4121a34
Binary files /dev/null and b/system/apps/nextcloud/icon.png differ
diff --git a/system/apps/openfire/app.json b/system/apps/openfire/app.json
new file mode 100644
index 0000000..ff1a728
--- /dev/null
+++ b/system/apps/openfire/app.json
@@ -0,0 +1,12 @@
+{
+ "app_name" : "app_openfire",
+ "caption" : "OpenFire",
+ "http_uri" : "http://[server_name]:9090/",
+ "use_http" : true,
+ "https_uri" : "https://[server_name]:9091/",
+ "use_https" : true,
+ "frame_support" : false,
+ "require" : [
+ {"data" : "openfire"}
+ ]
+}
diff --git a/system/apps/openfire/icon.png b/system/apps/openfire/icon.png
new file mode 100644
index 0000000..d79972f
Binary files /dev/null and b/system/apps/openfire/icon.png differ
diff --git a/system/apps/owncloud/app.json b/system/apps/owncloud/app.json
new file mode 100644
index 0000000..967d674
--- /dev/null
+++ b/system/apps/owncloud/app.json
@@ -0,0 +1,12 @@
+{
+ "app_name" : "app_owncloud",
+ "caption" : "OwnCloud",
+ "http_uri" : "http://[server_name]/owncloud/",
+ "use_http" : true,
+ "https_uri" : "https://[server_name]/owncloud/",
+ "use_https" : true,
+ "frame_support" : true,
+ "require" : [
+ {"data" : "php"}
+ ]
+}
diff --git a/system/apps/owncloud/icon.png b/system/apps/owncloud/icon.png
new file mode 100644
index 0000000..57d6ff8
Binary files /dev/null and b/system/apps/owncloud/icon.png differ
diff --git a/system/apps/phpmyadmin/app.json b/system/apps/phpmyadmin/app.json
new file mode 100644
index 0000000..fb090a6
--- /dev/null
+++ b/system/apps/phpmyadmin/app.json
@@ -0,0 +1,12 @@
+{
+ "app_name" : "app_phpmyadmin",
+ "caption" : "phpMyAdmin",
+ "http_uri" : "",
+ "use_http" : true,
+ "https_uri" : "https://[server_name]/phpMyAdmin/",
+ "use_https" : true,
+ "frame_support" : false,
+ "require" : [
+ {"data" : "php"}
+ ]
+}
diff --git a/system/apps/phpmyadmin/icon.png b/system/apps/phpmyadmin/icon.png
new file mode 100644
index 0000000..eb866ce
Binary files /dev/null and b/system/apps/phpmyadmin/icon.png differ
diff --git a/system/apps/phppgadmin/app.json b/system/apps/phppgadmin/app.json
new file mode 100644
index 0000000..9165817
--- /dev/null
+++ b/system/apps/phppgadmin/app.json
@@ -0,0 +1,12 @@
+{
+ "app_name" : "app_phppgadmin",
+ "caption" : "phpPgAdmin",
+ "http_uri" : "",
+ "use_http" : true,
+ "https_uri" : "https://[server_name]/phpPgAdmin/",
+ "use_https" : true,
+ "frame_support" : true,
+ "require" : [
+ {"data" : "php"}
+ ]
+}
diff --git a/system/apps/phppgadmin/icon.png b/system/apps/phppgadmin/icon.png
new file mode 100644
index 0000000..e238cd8
Binary files /dev/null and b/system/apps/phppgadmin/icon.png differ
diff --git a/system/apps/plex/app.json b/system/apps/plex/app.json
new file mode 100644
index 0000000..5e321f7
--- /dev/null
+++ b/system/apps/plex/app.json
@@ -0,0 +1,12 @@
+{
+ "app_name" : "app_plex",
+ "caption" : "Plex",
+ "http_uri" : "http://[server_name]:32400/web/index.html",
+ "use_http" : false,
+ "https_uri" : "https://[server_name]:32400/web/index.html",
+ "use_https" : true,
+ "frame_support" : false,
+ "require" : [
+ {"data" : "plexmediaserver"}
+ ]
+}
diff --git a/system/apps/plex/icon.png b/system/apps/plex/icon.png
new file mode 100644
index 0000000..355a179
Binary files /dev/null and b/system/apps/plex/icon.png differ
diff --git a/system/apps/rslsync/app.json b/system/apps/rslsync/app.json
new file mode 100644
index 0000000..9561033
--- /dev/null
+++ b/system/apps/rslsync/app.json
@@ -0,0 +1,12 @@
+{
+ "app_name" : "app_rslsync",
+ "caption" : "Resilio sync",
+ "http_uri" : "http://[server_name]:8888/",
+ "use_http" : true,
+ "https_uri" : "",
+ "use_https" : true,
+ "frame_support" : false,
+ "require" : [
+ {"data" : "rslsync"}
+ ]
+}
diff --git a/system/apps/rslsync/icon.png b/system/apps/rslsync/icon.png
new file mode 100644
index 0000000..e95f96a
Binary files /dev/null and b/system/apps/rslsync/icon.png differ
diff --git a/system/apps/transmission/app.json b/system/apps/transmission/app.json
new file mode 100644
index 0000000..837c59d
--- /dev/null
+++ b/system/apps/transmission/app.json
@@ -0,0 +1,12 @@
+{
+ "app_name" : "app_transmission",
+ "caption" : "Transmission",
+ "http_uri" : "http://[server_name]:9091/",
+ "use_http" : true,
+ "https_uri" : "",
+ "use_https" : false,
+ "frame_support" : true,
+ "require" : [
+ {"data" : "transmission"}
+ ]
+}
diff --git a/system/apps/transmission/icon.png b/system/apps/transmission/icon.png
new file mode 100644
index 0000000..d9805bd
Binary files /dev/null and b/system/apps/transmission/icon.png differ
diff --git a/system/classes/CAI/CAICP/src/Applications.php b/system/classes/CAI/CAICP/src/Applications.php
new file mode 100644
index 0000000..7524696
--- /dev/null
+++ b/system/classes/CAI/CAICP/src/Applications.php
@@ -0,0 +1,154 @@
+
+ * @copyright : Copyright (c) 2017 Alexander I. Chebykin
+ * @version : 0.9
+ * @license : MIT
+ * @link : https://github.com/CAI79/CAI-CP
+ ******************************************************************/
+
+namespace CAI\CAICP;
+
+class Applications
+{
+ protected $apps_avail = array();
+ protected $apps_enabled = array();
+ protected $cp_root = '';
+
+ /**
+ * Constructor
+ *
+ * @param string $root_dir Control panel's root directory
+ */
+ function __construct($root_dir)
+ {
+ $this->cp_root = filter_input(INPUT_SERVER, 'DOCUMENT_ROOT') . $root_dir;
+ $this->enum_enabled_apps();
+ $this->enum_avail_apps();
+ }
+
+ /**
+ * Read available applications info
+ */
+ private function enum_avail_apps()
+ {
+ $dir = opendir($this->cp_root . '/system/apps');
+ while($file = readdir($dir)) {
+ if (is_dir($this->cp_root . '/system/apps/' . $file)
+ && $file != '.' && $file != '..'
+ && file_exists($this->cp_root . '/system/apps/' . $file . '/app.json')) {
+ $app_info = json_decode(file_get_contents($this->cp_root .
+ '/system/apps/' .
+ $file .
+ '/app.json'),
+ true);
+ if (!array_key_exists('version', $app_info)) {
+ $app_info['version'] = '';
+ }
+ if (!array_key_exists('author', $app_info)) {
+ $app_info['author'] = '';
+ }
+ $this->apps_avail[$file] = array('caption' => $app_info['caption'],
+ 'version' => $app_info['version'],
+ 'author' => $app_info['author'],
+ 'enabled' => array_key_exists($app_info['caption'],
+ $this->apps_enabled));
+ }
+ }
+ }
+
+ /**
+ * Returns enabled applications list
+ *
+ * @return array enabled applications list
+ */
+ public function enum_enabled_apps() {
+ $this->apps_enabled = json_decode(file_get_contents($this->cp_root .
+ '/system/apps/apps.json'),
+ true);
+ }
+
+ /**
+ * Returns available applications list
+ *
+ * @return array available applications list
+ */
+ public function get_avail() {
+ return $this->apps_avail;
+ }
+
+ /**
+ * Returns available applications list in json format
+ *
+ * @return string available applications list in json format
+ */
+ public function get_avail_json() {
+ return html_entity_decode(json_encode($this->get_avail()));
+ }
+
+ /**
+ * Returns enabled applications list
+ *
+ * @return array enabled applications list
+ */
+ public function get_enabled() {
+ return $this->apps_enabled;
+ }
+
+ /**
+ * Returns enabled applications list in json format
+ *
+ * @return string enabled applications list
+ */
+ public function get_enabled_json() {
+ return html_entity_decode(json_encode($this->get_enabled()));
+ }
+
+ /**
+ * Enable apps
+ *
+ * @param array $values Array with apps
+ */
+ public function enable($values)
+ {
+ $this->apps_enabled = $values;
+
+ }
+
+ /**
+ * Enable apps
+ *
+ * @param string $json Apps in JSON format
+ */
+ public function enable_json($json)
+ {
+ $this->enable(json_decode($json, true));
+ }
+
+ /**
+ * Set list of enabled applications
+ *
+ * @param array $values array with enabled applications
+ */
+ public function set($values)
+ {
+ $this->apps_enabled = $values;
+ }
+
+ /**
+ * Saves enabled applications list
+ *
+ * @return bool
+ */
+ public function save()
+ {
+ return file_put_contents($this->cp_root . '/system/apps/apps.json',
+ html_entity_decode(json_encode($this->apps_enabled)));
+ }
+}
diff --git a/system/classes/CAI/CAICP/src/Localization.php b/system/classes/CAI/CAICP/src/Localization.php
new file mode 100644
index 0000000..5b67028
--- /dev/null
+++ b/system/classes/CAI/CAICP/src/Localization.php
@@ -0,0 +1,51 @@
+
+ * @copyright : Copyright (c) 2017 Alexander I. Chebykin
+ * @version : 1.0
+ * @license : MIT
+ * @link : https://github.com/CAI79/CAI-CP
+ ******************************************************************/
+
+namespace CAI\CAICP;
+
+class Localization
+{
+ protected $cp_root;
+ protected $lang;
+ protected $locale;
+
+ /**
+ * Constructor
+ *
+ * @param string $root_dir Control panel's root directory
+ * @param string $settings Control panel settings
+ */
+ function __construct($root_dir, $settings)
+ {
+ $this->cp_root = filter_input(INPUT_SERVER, 'DOCUMENT_ROOT') . $root_dir;
+
+ $this->lang = filter_input(INPUT_GET, 'lang', FILTER_SANITIZE_STRING);
+ if (!array_key_exists($this->lang, $settings['langs'])) {
+ $this->lang = $settings['lang'];
+ }
+ if ($this->lang == '') {
+ $this->lang = 'en';
+ }
+ $this->locale = json_decode(file_get_contents($this->cp_root .
+ '/system/json/locale/' .
+ $this->lang . '.json'),
+ true);
+ }
+
+ public function translate($id)
+ {
+ return $this->locale[$id];
+ }
+}
diff --git a/system/classes/CAI/CAICP/src/PSR4Autoloader.php b/system/classes/CAI/CAICP/src/PSR4Autoloader.php
new file mode 100644
index 0000000..df8f119
--- /dev/null
+++ b/system/classes/CAI/CAICP/src/PSR4Autoloader.php
@@ -0,0 +1,192 @@
+register();
+ *
+ * // register the base directories for the namespace prefix
+ * $loader->addNamespace('Foo\Bar', '/path/to/packages/foo-bar/src');
+ * $loader->addNamespace('Foo\Bar', '/path/to/packages/foo-bar/tests');
+ *
+ * The following line would cause the autoloader to attempt to load the
+ * \Foo\Bar\Qux\Quux class from /path/to/packages/foo-bar/src/Qux/Quux.php:
+ *
+ * prefixes[$prefix]) === false) {
+ $this->prefixes[$prefix] = array();
+ }
+
+ // retain the base directory for the namespace prefix
+ if ($prepend) {
+ array_unshift($this->prefixes[$prefix], $base_dir);
+ } else {
+ array_push($this->prefixes[$prefix], $base_dir);
+ }
+ }
+
+ /**
+ * Loads the class file for a given class name.
+ *
+ * @param string $class The fully-qualified class name.
+ * @return mixed The mapped file name on success, or boolean false on
+ * failure.
+ */
+ public function loadClass($class)
+ {
+ // the current namespace prefix
+ $prefix = $class;
+
+ // work backwards through the namespace names of the fully-qualified
+ // class name to find a mapped file name
+ while (false !== $pos = strrpos($prefix, '\\')) {
+
+ // retain the trailing namespace separator in the prefix
+ $prefix = substr($class, 0, $pos + 1);
+
+ // the rest is the relative class name
+ $relative_class = substr($class, $pos + 1);
+
+ // try to load a mapped file for the prefix and relative class
+ $mapped_file = $this->loadMappedFile($prefix, $relative_class);
+ if ($mapped_file) {
+ return $mapped_file;
+ }
+
+ // remove the trailing namespace separator for the next iteration
+ // of strrpos()
+ $prefix = rtrim($prefix, '\\');
+ }
+
+ // never found a mapped file
+ return false;
+ }
+
+ /**
+ * Load the mapped file for a namespace prefix and relative class.
+ *
+ * @param string $prefix The namespace prefix.
+ * @param string $relative_class The relative class name.
+ * @return mixed Boolean false if no mapped file can be loaded, or the
+ * name of the mapped file that was loaded.
+ */
+ protected function loadMappedFile($prefix, $relative_class)
+ {
+ // are there any base directories for this namespace prefix?
+ if (isset($this->prefixes[$prefix]) === false) {
+ return false;
+ }
+
+ // look through base directories for this namespace prefix
+ foreach ($this->prefixes[$prefix] as $base_dir) {
+
+ // replace the namespace prefix with the base directory,
+ // replace namespace separators with directory separators
+ // in the relative class name, append with .php
+ $file = $base_dir
+ . str_replace('\\', '/', $relative_class)
+ . '.php';
+
+ // if the mapped file exists, require it
+ if ($this->requireFile($file)) {
+ // yes, we're done
+ return $file;
+ }
+ }
+
+ // never found it
+ return false;
+ }
+
+ /**
+ * If a file exists, require it from the file system.
+ *
+ * @param string $file The file to require.
+ * @return bool True if the file exists, false if not.
+ */
+ protected function requireFile($file)
+ {
+ if (file_exists($file)) {
+ require $file;
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/system/classes/CAI/CAICP/src/Settings.php b/system/classes/CAI/CAICP/src/Settings.php
new file mode 100644
index 0000000..572619d
--- /dev/null
+++ b/system/classes/CAI/CAICP/src/Settings.php
@@ -0,0 +1,123 @@
+
+ * @copyright : Copyright (c) 2017 Alexander I. Chebykin
+ * @version : 0.9
+ * @license : MIT
+ * @link : https://github.com/CAI79/CAI-CP
+ ******************************************************************/
+
+namespace CAI\CAICP;
+
+class Settings
+{
+ protected $user_settings = array();
+ protected $cp_root = '';
+
+ /**
+ * Constructor
+ *
+ * @param string $root_dir Control panel's root directory
+ */
+ function __construct($root_dir)
+ {
+ $this->cp_root = filter_input(INPUT_SERVER, 'DOCUMENT_ROOT') . $root_dir;
+ $this->load();
+ }
+
+ /**
+ * Read settings from disk
+ */
+ private function load()
+ {
+ $this->user_settings = json_decode(
+ file_get_contents($this->cp_root . '/system/json/settings.json'),
+ true);
+ }
+
+ /**
+ * Save settings to disk
+ *
+ * @return false|written bytes count
+ */
+ public function save()
+ {
+ return file_put_contents($this->cp_root . '/system/json/settings.json',
+ html_entity_decode(json_encode($this->user_settings)));
+ }
+
+ /**
+ * Return array with settings
+ *
+ * @return array
+ */
+ public function get()
+ {
+ return $this->user_settings;
+ }
+
+ /**
+ * Return string with settings in JSON format
+ *
+ * @return string
+ */
+ public function get_json()
+ {
+ return html_entity_decode(json_encode($this->get()));
+ }
+
+ /**
+ * Return option value
+ *
+ * @param string $option_name Option name
+ *
+ * @return variant|boolean
+ */
+ public function get_value($option_name)
+ {
+ if (trim($option_name) !== '') {
+ return $this::user_settings[$option_name];
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Set all settings
+ *
+ * @param array $values Array with settings
+ */
+ public function set($values)
+ {
+ $this->user_settings = $values;
+ }
+
+ /**
+ * Set all settings
+ *
+ * @param string $json Settings in JSON format
+ */
+ public function set_json($json)
+ {
+ $this->set(json_decode($json, true));
+ }
+
+ /**
+ * Set option value
+ *
+ * @param string $option_name Option name
+ * @param variant $value New value
+ */
+ public function set_value($option_name, $value)
+ {
+ if (trim($option_name) !== '') {
+ $this->user_settings[$option_name] = $value;
+ }
+ }
+}
diff --git a/system/help/css/about.css b/system/help/css/about.css
new file mode 100644
index 0000000..b274a99
--- /dev/null
+++ b/system/help/css/about.css
@@ -0,0 +1,32 @@
+.hidden {display: none;}
+.centered {text-align: center;}
+
+.help_title {font-weight: bold;}
+
+.help_ver, .span_ver,
+.span_ver_date {font-style: italic;}
+
+.span_author,
+.span_author_ru {}
+
+html,
+body {
+ height: 100%;
+ margin: 0;
+}
+
+hr {
+ margin: 5px 0;
+ padding: 0;
+ border: 0;
+ height: 1px;
+ background-image: -webkit-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0));
+ background-image: -moz-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0));
+ background-image: -ms-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0));
+ background-image: -o-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0));
+ background: linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0, 0));
+}
+
+article {
+ margin: 10px;
+}
\ No newline at end of file
diff --git a/system/help/css/help.css b/system/help/css/help.css
new file mode 100644
index 0000000..9659c91
--- /dev/null
+++ b/system/help/css/help.css
@@ -0,0 +1,89 @@
+.hidden {display: none;}
+.centered {text-align: center;}
+
+.help_title {font-weight: bold;}
+
+.help_ver, .span_ver,
+.span_ver_date {font-style: italic;}
+
+.tbl_wide {
+ width: 100%;
+ border: solid 1px #ccc;
+}
+.tbl_wide .tbl_header {
+ font-weight: bold;
+ text-align: center;
+ background-color: #ccc;
+}
+.tbl_wide td {
+ padding: 5px;
+ border: solid 1px #ccc;
+}
+
+.mnu_selected {
+ background-color: #000;
+ color: #fff;
+}
+
+.scrollable {overflow: auto;}
+
+html,
+body {
+ height: 100%;
+ margin: 0;
+}
+
+hr {
+ margin: 5px 0;
+ padding: 0;
+ border: 0;
+ height: 1px;
+ background-image: -webkit-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0));
+ background-image: -moz-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0));
+ background-image: -ms-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0));
+ background-image: -o-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0,0));
+ background: linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.25),rgba(0,0,0, 0));
+}
+
+nav {
+ width: 200px;
+ box-shadow: 5px 5px 5px #ccc;
+ position: fixed;
+ padding: 2px;
+ margin-left: 10px;
+ border: solid 1px #ccc;
+}
+nav ul {
+ padding: 0;
+ margin: 2px;
+ list-style: none;
+ transition: all 0.5s ease-out 0.1s;
+}
+nav ul li {
+ cursor: pointer;
+}
+nav ul li ul {
+ padding: 0 0 0 2px;
+}
+nav ul li div {
+ padding: 2px 2px 2px 4px;
+}
+nav ul li div:hover {
+ background-color: #ddd;
+ color: #000;
+}
+
+article {
+ margin: 2px 2px 2px 225px;
+ transition: all 0.5s ease-out 0.1s;
+}
+
+a, a:visited, a:hover {
+ color: #555;
+ text-decoration: none;
+}
+a:hover {text-decoration: underline;}
+
+#div_license {
+ height: 150px;
+}
\ No newline at end of file
diff --git a/system/help/index.html b/system/help/index.html
new file mode 100644
index 0000000..4ca3308
--- /dev/null
+++ b/system/help/index.html
@@ -0,0 +1,13 @@
+
+
+
+ CAI CP: Help
+
+
+
+
+
+ Loading...
+
+
+
diff --git a/system/help/js/help.js b/system/help/js/help.js
new file mode 100644
index 0000000..6a681ed
--- /dev/null
+++ b/system/help/js/help.js
@@ -0,0 +1,201 @@
+// MIT License:
+//
+// Copyright (c) 2016-2017, Alexander I. Chebykin
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+/**
+ * CAI CP v.1
+ *
+ * @module : Help subsystem
+ * @author : Alexander I. Chebykin
+ * @copyright : Copyright (c) 2016-2017 Alexander I. Chebykin
+ * @version : 0.9
+ * @build date : 2017-06-16
+ * @license : MIT
+ * @link : https://github.com/CAI79/CAI-CP
+ ******************************************************************/
+
+(function () {
+ 'use strict';
+
+ /**
+ * Return GET parameter
+ *
+ * @param {string} key Key name
+ *
+ * returns {string}
+ */
+ function $_GET(key) {
+ var s = window.location.search;
+
+ s = s.match(new RegExp(key + '=([^&=]+)'));
+
+ return s ? s[1] : false;
+ }
+
+ /**
+ * HelpSystem constructor
+ *
+ * @returns {undefined}
+ */
+ function HelpSystem() {
+ this.locale = 'en';
+ }
+
+ /**
+ *
+ * @param {int} init_level Initialization level: 1 - select locale
+ * 2 - redirect to localized resource
+ * 3 - load version info
+ * 4 - set event handlers
+ *
+ * @returns {undefined}
+ */
+ HelpSystem.prototype.init = function (init_level) {
+ var help_instance = this;
+
+ switch (init_level) {
+ case 1:
+ if ($_GET('lang') !== '') {
+ this.locale = $_GET('lang');
+ } else {
+ var request = new XMLHttpRequest(),
+ res_data;
+
+ if (!~location.href.indexOf('locale')) {
+ request.open('GET', '../json/settings.json');
+ } else {
+ request.open('GET', '../../../json/settings.json');
+ }
+ request.send();
+
+ request.onreadystatechange = function () {
+ if (this.readyState === 4) {
+ if (this.status === 200) {
+ res_data = JSON.parse(this.responseText);
+
+ help_instance.locale = res_data.lang;
+ }
+ }
+ };
+ }
+ this.init(2);
+ break;
+ case 2:
+ if (!~location.href.indexOf('locale')) {
+ location.href = 'locale/' + this.locale + location.hash;
+ return;
+ } else {
+ this.init(3);
+ }
+ break;
+ case 3:
+ var request = new XMLHttpRequest(),
+ res_data;
+
+ if (!~location.href.indexOf('locale')) {
+ request.open('GET', '../json/version.json');
+ } else {
+ request.open('GET', '../../../json/version.json');
+ }
+ request.send();
+
+ request.onreadystatechange = function (e) {
+ if (this.readyState === 4) {
+ if (this.status === 200) {
+ res_data = JSON.parse(this.responseText);
+
+ var vers = document.getElementsByClassName('span_ver'),
+ ver_dates = document.getElementsByClassName('span_ver_date');
+
+ for (var i = 0; i < vers.length; i++) {
+ vers[i].textContent = res_data.version;
+ }
+ for (var i = 0; i < ver_dates.length; i++) {
+ ver_dates[i].textContent = new Date(res_data.build_date).toLocaleDateString();
+ }
+ }
+ }
+ };
+ this.init(4);
+ break;
+ case 4:
+ var nav_list = document.getElementsByTagName('nav'),
+ toc_lis = document.getElementById('menu_toc').getElementsByTagName('li');
+
+ // Show menu
+ for (var i = 0; i < nav_list.length; i++) {
+ nav_list[i].classList.remove('hidden');
+ }
+
+ // Add event listeners
+ for (var i = 0; i < toc_lis.length; i++) {
+ toc_lis[i].getElementsByTagName('div')[0].addEventListener('click', function (event) {
+ help_instance.select_topic(this.parentElement.id.substr(4));
+ });
+ }
+
+ if (location.hash !== '') {
+ help_instance.select_topic(location.hash.substr(1));
+ } else {
+ help_instance.select_topic('start');
+ }
+ }
+ };
+
+ /**
+ *
+ * @param {string} topic_id Topic identifier
+ *
+ * @returns {undefined}
+ */
+ HelpSystem.prototype.select_topic = function (topic_id) {
+ var articles = document.getElementsByTagName('article'),
+ toc_divs = document.getElementsByClassName('div_toc');
+
+ for (var j = 0; j < toc_divs.length; j++) {
+ toc_divs[j].classList.remove('mnu_selected');
+ }
+
+ document.getElementById('mnu_' + topic_id).getElementsByTagName('div')[0].classList.add('mnu_selected');
+
+ for (var j = 0; j < articles.length; j++) {
+ if (articles[j].id === 'art_' + topic_id) {
+ articles[j].classList.remove('hidden');
+ } else {
+ articles[j].classList.add('hidden');
+ }
+ }
+ location.hash = topic_id;
+ };
+
+ document.addEventListener('DOMContentLoaded', function () {
+ 'use strict';
+
+ /**
+ * Initialize help object
+ */
+ var help = new HelpSystem();
+
+ help.init(1);
+
+ });
+
+}());
diff --git a/system/help/locale/en/index.html b/system/help/locale/en/index.html
new file mode 100644
index 0000000..95eb1e5
--- /dev/null
+++ b/system/help/locale/en/index.html
@@ -0,0 +1,230 @@
+
+
+
+ CAI CP: Help
+
+
+
+
+
+
+
+
Tips and ticks
+
+
Table witgets can be maximized by double click on header.
+
You can switch between information widgets blocks with
+ following hotkeys:
+
+
Ctrl + 1: System
+
Ctrl + 2: System pulse
+
Ctrl + 3: Storage
+
Ctrl + 4: Network
+
Ctrl + 5: SMB
+
Ctrl + Left arrow: Previous block
+
Ctrl + Right arrow: Next block
+
+ Also You can use left or right swipe on header on devices
+ with touchscreen.
+
+
+
Ctrl + F1: This help
+
+
+
+
+
+
System requirements
+
Control panel core uses following commands and utilities:
+
+
gawk (GNU awk)
+
grep
+
hddtemp
+
lsb-release
+
sed
+
smartmontools (smartctl)
+
sysstat (iostat)
+
php7
+
+
+ Applications may have other dependencies.
+
+
+
+
Setting up
+
S.M.A.R.T. and HDD temperature monitoring widgets requires super
+ user priviledges. You need to add webserver account to sudoers.
+ Just run visudo command and add to User privilege specification
+ section next lines:
+
SMB monitoring widgets requires super user priveledges. You need
+ to add webserver account to sudoers. Just run visudo command and
+ add to User privilege specification section next lines:
+
+ wwwrun ALL=NOPASSWD: /usr/bin/smbstatus
+
+
+
+ Control panel configuration file is /system/json/settings.json.
+ Available options:
+
+
+
Option
+
Possible values
+
Description
+
+
+
lang
+
ru en ...
+
Default languag. You can specify one of installed
+ languages.
+
+
+
+
langs
+
"en" : "English"
+ "ru" : "Русский"
+ ...
+
+
Available languages.
+
+
+
dim_on_create
+
true false
+
Widgets runs semi-transparent until data is loaded.
+
+
+
check_files_rights
+
true false
+
Check files and directories rights on startup.
+
+
+
check_hdd_temp_interval
+
0 .. 86400
+
HDD temperature checking interval (in seconds).
+
+
+
check_smart_interval
+
0 .. 86400
+
HDD S.M.A.R.T checking interval (in seconds).
+
+
+
+
+ You can find applications list in /system/apps/apps.json.
+ Control panel checks dependencies and run applications if they
+ are ok. You can manually disable application by removing string
+ with it from this file.
+
+
+
+
Security
+
+
You need to set owner to control panel files to wwwrun:www,
+ all directories and .sh files must have 550 mask, other
+ directories - 440 mask.
+
You need to set 640 mask to /system/json/settings.json if
+ You want to change some settings from WebUI.
+
You can set rights by executing following commands:
+
+ $ chown -R wwwrun:www /path/to/base/dir
+ $ chmod 440 $(find /path/to/base/dir -type f)
+ $ chmod 550 $(find /path/to/base/dir -type d)
+ $ chmod 550 $(find /path/to/base/dir -type f | grep .sh$)
+ $ chmod 640 /path/to/base/dir/system/json/settings.json
+ $ chmod 640 /path/to/base/dir/system/apps/apps.json
+
+
+
+
Deny accss to .sh files by adding next options to
+
<Directory></Directory>
+ block of Apache settings:
+
+ <FilesMatch "\.(sh)$">
+ Require all denied
+ </FilesMatch>
+
+
+
Set password to restrict access. Basic autorization example:
+ run Apache's utility htpasswd to generate password file
+ .htpasswd. Add next lines to Apache's config file into
+
+ Attention! Basic authorization uses plain text to send
+ passwords. Consider to use HTTPS to avoid password leak.
+
+
+
You need to add wwwrun user to sudoers if to enable
+ shutdown or reboot server functions. Be carefully with this
+ option!
+
Run # visudo and add following line to
+ "User privilege specification":
+ wwwrun ALL=NOPASSWD: /sbin/shutdown
+
+
+
+
+
+
About
+
CAI CP
+
Version _._. Build date __.__.____
+
Author: Alexander Chebykin
+
License: MIT
+
+
+MIT License
+-----------
+
+Copyright (c) 2016-2017, Alexander I. Chebykin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+
+
+
diff --git a/system/help/locale/ru/index.html b/system/help/locale/ru/index.html
new file mode 100644
index 0000000..fd4e8f6
--- /dev/null
+++ b/system/help/locale/ru/index.html
@@ -0,0 +1,268 @@
+
+
+
+ CAI CP: Help
+
+
+
+
+
+
+
+
Советы по использованию
+
+
Табличные виджеты можно распахнуть на весь экран двойным
+ щелчком по заголовку.
+
Для переключения по информационным блокам используйте
+ следующие комбинации клавиш:
+
+
Ctrl + 1: Система
+
Ctrl + 2: Пульс системы
+
Ctrl + 3: Файловое хранилище
+
Ctrl + 4: Сеть
+
Ctrl + 5: SMB
+
Ctrl + Влево: Предыдущий блок
+
Ctrl + Вправо: Следующий блок
+
+ Также вы можете переключаться между блоками, проведя влево
+ или вправо по заголовку на устройствах с сенсорным экраном.
+
+
+
Ctrl + F1: Справка
+
+
+
+
+
+
Системные требования
+
Ядро системы использует следующие команды и утилиты:
+
+
gawk (GNU awk)
+
grep
+
hddtemp
+
lsb-release
+
sed
+
smartmontools (smartctl)
+
sysstat (iostat)
+
php7
+
+
+ Различные приложения могут требовать наличие других программ и утилит.
+
+
+
+
Настройка
+
Для мониторинга S.M.A.R.T. и температуры дисков требуется
+ разрешить выполнение smartctl и hddtemp веб-сервером. Для этого
+ запустите редактор sudo командой visudo и добавьте в раздел User
+ privilege specification следующие строки:
+
Для мониторинга ресурсов SMB требуется разрешить выполнение
+ smbstatus веб-сервером. Для этого запустите редактор sudo
+ командой visudo и добавьте в раздел User privilege specification
+ следующие строки:
+
+ wwwrun ALL=NOPASSWD: /usr/bin/smbstatus
+
+
+
+ Конфигурация панели управления задаётся в файле /system/json/settings.json.
+ Параметры:
+
+
+
Параметр
+
Возможные значения
+
Описание
+
+
+
lang
+
ru en ...
+
Язык по умолчанию. Может быть указан один из
+ доступных языков.
+
+
+
+
langs
+
"en" : "English"
+ "ru" : "Русский"
+ ...
+
+
Доступные языки.
+
+
+
dim_on_create
+
true false
+
Виджеты отображаются полупрозрачными пока не
+ загрузятся данные.
+
+
+
check_files_rights
+
true false
+
При запуске проводится проверка правильности
+ установленных прав на файлы и каталоги.
+
+
+
check_hdd_temp_interval
+
0 .. 86400
+
Интервал (в секундах) проверки температуры жёстких
+ дисков.
+
+
+
check_smart_interval
+
0 .. 86400
+
Интервал (в секундах) проверки состояния S.M.A.R.T.
+ жёстких дисков.
+
+
+
+
+ Список загружаемых приложений находится в файле /system/apps/apps.json.
+ Панель управления проверяет зависимости приложений и отключает
+ те, что не могут быть запущены на данном сервере. Если вы хотите
+ принудительно отключить какое-либо приложение, удалите строку
+ с его названием из данного файла.
+
+
+
+
Безопасность
+
+
Назначьте владельцем файлов и каталогов wwwrun:www, всем
+ каталогам и файлам .sh назначьте права по маске 550,
+ остальным файлам - 440.
+
Если хотите иметь возможность управлять некоторыми
+ параметрами из веб-интерфейса, установите маску 640 файлу
+ /system/json/settings.json.
Запретите доступ через веб к файлам с расширением .sh, для
+ этого в конфигурационный файл Apache в блок
+
<Directory></Directory>
+ добавьте следующие строки:
+
+ <FilesMatch "\.(sh)$">
+ Require all denied
+ </FilesMatch>
+
+
+
Установите пароль на доступ к панели управления. Пример для
+ настройки базовой аутентификации: с помощью утилиты htpasswd
+ (входит в комплект сервера Apache) создайте содержащий пароль
+ файл .htpasswd. В конфигурационный файл Apache добавьте в блок
+
+ Обратите внимание, что в рассмотренном случае используется
+ базовая аутентификация, при её использовании пароль
+ передаётся в открытом виде, для защиты передаваемой
+ информации настройте доступ по протоколу HTTPS.
+
+
+
Если хотите включить возможность перезагрузки и отключения
+ сервера, следует добавить пользователя wwwrun в sudoers.
+ Будьте осторожны с данной возможностью!
+
Запустите # visudo и добавьте следующую
+ строку в блок "User privilege specification":
+ wwwrun ALL=NOPASSWD: /sbin/shutdown
+
+
+
+
+
+
О панели управления
+
CAI CP
+
Версия _._ от __.__.____
+
Автор: Александр Чебыкин
+
Лицензия: MIT
+
+
+Лицензия MIT
+------------
+
+Copyright (c) 2016-2017, Александр Чебыкин
+
+Данная лицензия разрешает лицам, получившим копию данного программного
+обеспечения и сопутствующей документации (в дальнейшем именуемыми
+«Программное Обеспечение»), безвозмездно использовать Программное Обеспечение
+без ограничений, включая неограниченное право на использование, копирование,
+изменение, слияние, публикацию, распространение, сублицензирование и/или
+продажу копий Программного Обеспечения, а также лицам, которым предоставляется
+данное Программное Обеспечение, при соблюдении следующих условий:
+
+Указанное выше уведомление об авторском праве и данные условия должны быть
+включены во все копии или значимые части данного Программного Обеспечения.
+
+ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО
+ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ ГАРАНТИИ ТОВАРНОЙ
+ПРИГОДНОСТИ, СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И ОТСУТСТВИЯ НАРУШЕНИЙ,
+НО НЕ ОГРАНИЧИВАЯСЬ ИМИ. НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ
+ОТВЕТСТВЕННОСТИ ПО КАКИМ-ЛИБО ИСКАМ, ЗА УЩЕРБ ИЛИ ПО ИНЫМ ТРЕБОВАНИЯМ, В ТОМ
+ЧИСЛЕ, ПРИ ДЕЙСТВИИ КОНТРАКТА, ДЕЛИКТЕ ИЛИ ИНОЙ СИТУАЦИИ, ВОЗНИКШИМ ИЗ-ЗА
+ИСПОЛЬЗОВАНИЯ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫХ ДЕЙСТВИЙ С ПРОГРАММНЫМ
+ОБЕСПЕЧЕНИЕМ.
+
+-------------------------------------------------------------------------------
+
+MIT License
+-----------
+
+Copyright (c) 2016-2017, Alexander I. Chebykin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+