From 4581722bbeff521f511a83f828e3becd880af041 Mon Sep 17 00:00:00 2001 From: James Gao Date: Tue, 8 Oct 2013 18:37:56 -0700 Subject: [PATCH 01/18] Less scary message with Ctrl-C, remove gcode viewer for android --- src/octoprint/server.py | 2 ++ src/octoprint/static/js/app/main.js | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/octoprint/server.py b/src/octoprint/server.py index 0bf86796..44056155 100644 --- a/src/octoprint/server.py +++ b/src/octoprint/server.py @@ -1207,6 +1207,8 @@ class Server(): printer.connect(port, baudrate) try: IOLoop.instance().start() + except KeyboardInterrupt: + logger.info("Goodbye!") except: logger.fatal("Now that is embarrassing... Something really really went wrong here. Please report this including the stacktrace below in OctoPrint's bugtracker. Thanks!") logger.exception("Stacktrace follows:") diff --git a/src/octoprint/static/js/app/main.js b/src/octoprint/static/js/app/main.js index e4cd786e..9cc5e913 100644 --- a/src/octoprint/static/js/app/main.js +++ b/src/octoprint/static/js/app/main.js @@ -1,4 +1,9 @@ $(function() { + //Detect mobile browsers and remove gcode pane + //from http://stackoverflow.com/questions/3514784/what-is-the-best-way-to-detect-a-handheld-device-in-jquery + if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { + $("#gcode, a[href='#gcode']").remove(); + } //~~ Initialize view models var loginStateViewModel = new LoginStateViewModel(); From 858873dfa03090d56298fecd649fd407854fe7e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 20 Jan 2014 15:33:08 +0100 Subject: [PATCH 02/18] Also provide the filename (basename without the path) in print events --- src/octoprint/util/comm.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 1a970495..4679fc22 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -341,6 +341,7 @@ class MachineCom(object): if self._currentFile is not None: payload = { "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() } eventManager().fire(Events.PRINT_FAILED, payload) @@ -372,6 +373,7 @@ class MachineCom(object): self._changeState(self.STATE_PRINTING) eventManager().fire(Events.PRINT_STARTED, { "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() }) @@ -438,6 +440,7 @@ class MachineCom(object): eventManager().fire(Events.PRINT_CANCELLED, { "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() }) @@ -454,6 +457,7 @@ class MachineCom(object): eventManager().fire(Events.PRINT_RESUMED, { "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() }) elif pause and self.isPrinting(): @@ -463,6 +467,7 @@ class MachineCom(object): eventManager().fire(Events.PRINT_PAUSED, { "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation() }) @@ -665,6 +670,7 @@ class MachineCom(object): self._changeState(self.STATE_OPERATIONAL) eventManager().fire(Events.PRINT_DONE, { "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation(), "time": time.time() - self._currentFile.getStartTime() }) @@ -939,6 +945,7 @@ class MachineCom(object): else: payload = { "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation(), "time": time.time() - self._currentFile.getStartTime() } From c5c382ade6ecd27f50e809c5785142964559990c Mon Sep 17 00:00:00 2001 From: Salandora Date: Mon, 27 Jan 2014 09:35:51 +0100 Subject: [PATCH 03/18] Log tab added to download and remove logs --- src/octoprint/server/__init__.py | 1 + src/octoprint/server/api/__init__.py | 1 + src/octoprint/server/api/log.py | 62 +++++++++++++++++++ src/octoprint/static/js/app/dataupdater.js | 6 +- src/octoprint/static/js/app/main.js | 6 +- src/octoprint/static/js/app/viewmodels/log.js | 62 +++++++++++++++++++ src/octoprint/templates/index.jinja2 | 37 +++++++++++ 7 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 src/octoprint/server/api/log.py create mode 100644 src/octoprint/static/js/app/viewmodels/log.js diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 1fccc3e3..8eb1f9e3 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -177,6 +177,7 @@ class Server(): self._tornado_app = Application(self._router.urls + [ (r"/downloads/timelapse/([^/]*\.mpg)", LargeResponseHandler, {"path": settings().getBaseFolder("timelapse"), "as_attachment": True}), (r"/downloads/files/local/([^/]*\.(gco|gcode))", LargeResponseHandler, {"path": settings().getBaseFolder("uploads"), "as_attachment": True}), + (r"/downloads/logs/([^/]*)", LargeResponseHandler, {"path": settings().getBaseFolder("logs"), "as_attachment": True}), (r".*", FallbackHandler, {"fallback": WSGIContainer(app.wsgi_app)}) ]) self._server = HTTPServer(self._tornado_app) diff --git a/src/octoprint/server/api/__init__.py b/src/octoprint/server/api/__init__.py index 5614bee8..9ff5ae54 100644 --- a/src/octoprint/server/api/__init__.py +++ b/src/octoprint/server/api/__init__.py @@ -27,6 +27,7 @@ from . import files as api_files from . import settings as api_settings from . import timelapse as api_timelapse from . import users as api_users +from . import log as api_logs #~~ first run setup diff --git a/src/octoprint/server/api/log.py b/src/octoprint/server/api/log.py new file mode 100644 index 00000000..0fda12dc --- /dev/null +++ b/src/octoprint/server/api/log.py @@ -0,0 +1,62 @@ +# coding=utf-8 +__author__ = "Marc Hannappel Salandora" +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' + +import os +import datetime + +from flask import request, jsonify, make_response, url_for +from werkzeug.utils import secure_filename + +import octoprint.util as util +from octoprint.settings import settings, valid_boolean_trues + +from octoprint.server import restricted_access, admin_permission +from octoprint.server.util import redirectToTornado +from octoprint.server.api import api + +@api.route("/logs", methods=["GET"]) +def getLogData(): + files = _getLogFiles() + return jsonify(files=files) + +@api.route("/logs/online/", methods=["GET"]) +def onlineLog(filename): + secure = os.path.join(settings().getBaseFolder("logs"), secure_filename(filename)) + if not os.path.exists(secure): + return make_response("Unknown filename: %s" % filename, 404) + + file = open(secure, 'r') + return jsonify(data=file.read()) + +@api.route("/logs/", methods=["GET"]) +def downloadLog(filename): + return redirectToTornado(request, url_for("index") + "downloads/logs/" + filename) + + +@api.route("/logs/", methods=["DELETE"]) +@restricted_access +def deleteLog(filename): + secure = os.path.join(settings().getBaseFolder("logs"), secure_filename(filename)) + if os.path.exists(secure): + os.remove(secure) + + return getLogData() + +def _getLogFiles(): + files = [] + basedir = settings().getBaseFolder("logs") + for osFile in os.listdir(basedir): + statResult = os.stat(os.path.join(basedir, osFile)) + files.append({ + "name": osFile, + "size": util.getFormattedSize(statResult.st_size), + "bytes": statResult.st_size, + "date": util.getFormattedDateTime(datetime.datetime.fromtimestamp(statResult.st_ctime)) + }) + + for file in files: + file["url"] = url_for("index") + "downloads/logs/" + file["name"] + + return files + diff --git a/src/octoprint/static/js/app/dataupdater.js b/src/octoprint/static/js/app/dataupdater.js index b7019ecf..3304fd64 100644 --- a/src/octoprint/static/js/app/dataupdater.js +++ b/src/octoprint/static/js/app/dataupdater.js @@ -1,4 +1,4 @@ -function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewModel, temperatureViewModel, controlViewModel, terminalViewModel, gcodeFilesViewModel, timelapseViewModel, gcodeViewModel) { +function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewModel, temperatureViewModel, controlViewModel, terminalViewModel, gcodeFilesViewModel, timelapseViewModel, gcodeViewModel, logViewModel) { var self = this; self.loginStateViewModel = loginStateViewModel; @@ -10,6 +10,7 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM self.gcodeFilesViewModel = gcodeFilesViewModel; self.timelapseViewModel = timelapseViewModel; self.gcodeViewModel = gcodeViewModel; + self.logViewModel = logViewModel; self._socket = undefined; self._autoReconnecting = false; @@ -38,7 +39,8 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM self._autoReconnectTrial = 0; if ($("#offline_overlay").is(":visible")) { - $("#offline_overlay").hide(); + $("#offline_overlay").hide(); + self.logViewModel.requestData(); self.timelapseViewModel.requestData(); self.loginStateViewModel.requestData(); self.gcodeFilesViewModel.requestData(); diff --git a/src/octoprint/static/js/app/main.js b/src/octoprint/static/js/app/main.js index f8270946..4c6b934c 100644 --- a/src/octoprint/static/js/app/main.js +++ b/src/octoprint/static/js/app/main.js @@ -14,6 +14,7 @@ $(function() { var gcodeFilesViewModel = new GcodeFilesViewModel(printerStateViewModel, loginStateViewModel); var gcodeViewModel = new GcodeViewModel(loginStateViewModel, settingsViewModel); var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel); + var logViewModel = new LogViewModel(loginStateViewModel); var dataUpdater = new DataUpdater( loginStateViewModel, @@ -24,7 +25,8 @@ $(function() { terminalViewModel, gcodeFilesViewModel, timelapseViewModel, - gcodeViewModel + gcodeViewModel, + logViewModel ); // work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at @@ -302,6 +304,7 @@ $(function() { ko.applyBindings(navigationViewModel, document.getElementById("navbar")); ko.applyBindings(appearanceViewModel, document.getElementsByTagName("head")[0]); ko.applyBindings(printerStateViewModel, document.getElementById("drop_overlay")); + ko.applyBindings(logViewModel, document.getElementById("logs")); var timelapseElement = document.getElementById("timelapse"); if (timelapseElement) { @@ -316,6 +319,7 @@ $(function() { gcodeFilesViewModel.requestData(); timelapseViewModel.requestData(); settingsViewModel.requestData(); + logViewModel.requestData(); loginStateViewModel.subscribe(function(change, data) { if ("login" == change) { diff --git a/src/octoprint/static/js/app/viewmodels/log.js b/src/octoprint/static/js/app/viewmodels/log.js new file mode 100644 index 00000000..15578885 --- /dev/null +++ b/src/octoprint/static/js/app/viewmodels/log.js @@ -0,0 +1,62 @@ +function LogViewModel(loginStateViewModel) { + var self = this; + + self.loginState = loginStateViewModel; + + // initialize list helper + self.listHelper = new ItemListHelper( + "logFiles", + { + "name": function(a, b) { + // sorts ascending + if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1; + if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1; + return 0; + }, + "creation": function(a, b) { + // sorts descending + if (a["date"] > b["date"]) return -1; + if (a["date"] < b["date"]) return 1; + return 0; + }, + "size": function(a, b) { + // sorts descending + if (a["bytes"] > b["bytes"]) return -1; + if (a["bytes"] < b["bytes"]) return 1; + return 0; + } + }, + { + }, + "name", + [], + [], + CONFIG_LOGFILESPERPAGE + ); + + self.requestData = function() { + $.ajax({ + url: API_BASEURL + "logs", + type: "GET", + dataType: "json", + success: self.fromResponse + }); + }; + + self.fromResponse = function(response) { + var files = response.files; + if (files === undefined) + return; + + self.listHelper.updateItems(files); + } + + self.removeFile = function(filename) { + $.ajax({ + url: API_BASEURL + "logs/" + filename, + type: "DELETE", + dataType: "json", + success: self.requestData + }); + } +} diff --git a/src/octoprint/templates/index.jinja2 b/src/octoprint/templates/index.jinja2 index 17f25b44..cae0178a 100644 --- a/src/octoprint/templates/index.jinja2 +++ b/src/octoprint/templates/index.jinja2 @@ -27,6 +27,7 @@ var CONFIG_GCODEFILESPERPAGE = 5; var CONFIG_TIMELAPSEFILESPERPAGE = 10; + var CONFIG_LOGFILESPERPAGE = 10; var CONFIG_USERSPERPAGE = 10; var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}"; var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %} @@ -252,6 +253,7 @@ {% if enableGCodeVisualizer %}
  • GCode Viewer
  • {% endif %}
  • Terminal
  • {% if enableTimelapse %}
  • Timelapse
  • {% endif %} +
  • Logs
  • @@ -585,6 +587,40 @@
    {% endif %} +
    +

    Logs

    + + + + + + + + + + + + + + + + + +
    NameSizeAction
     | 
    + +
    @@ -644,6 +680,7 @@ + From 3f7b222b92fb48817a168a4272128afcd3b91030 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Fri, 14 Feb 2014 11:33:53 -0800 Subject: [PATCH 04/18] fix up urls in API example requests --- docs/api/connection.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api/connection.rst b/docs/api/connection.rst index 27feb765..ee2f88f2 100644 --- a/docs/api/connection.rst +++ b/docs/api/connection.rst @@ -20,7 +20,7 @@ Get connection settings .. sourcecode:: http - GET /api/control/connection HTTP/1.1 + GET /api/connection HTTP/1.1 Host: example.com Content-Type: application/json X-Api-Key: abcdef... @@ -77,7 +77,7 @@ Issue a connection command .. sourcecode:: http - POST /api/control/connection HTTP/1.1 + POST /api/connection HTTP/1.1 Host: example.com Content-Type: application/json X-Api-Key: abcdef... @@ -94,7 +94,7 @@ Issue a connection command .. sourcecode:: http - POST /api/control/connection HTTP/1.1 + POST /api/connection HTTP/1.1 Host: example.com Content-Type: application/json X-Api-Key: abcdef... From a630b74fa818bdb0d6dd85ed17d1005d8af1a0a3 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Fri, 14 Feb 2014 20:55:42 -0800 Subject: [PATCH 05/18] fix crash when getStartTime() returns None --- src/octoprint/util/comm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 4679fc22..6a805d7e 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -672,7 +672,7 @@ class MachineCom(object): "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation(), - "time": time.time() - self._currentFile.getStartTime() + "time": self.getPrintTime() }) elif 'Done saving file' in line: self.refreshSdFiles() @@ -934,7 +934,7 @@ class MachineCom(object): payload = { "local": self._currentFile.getLocalFilename(), "remote": self._currentFile.getRemoteFilename(), - "time": time.time() - self._currentFile.getStartTime() + "time": self.getPrintTime() } self._currentFile = None @@ -947,7 +947,7 @@ class MachineCom(object): "file": self._currentFile.getFilename(), "filename": os.path.basename(self._currentFile.getFilename()), "origin": self._currentFile.getFileLocation(), - "time": time.time() - self._currentFile.getStartTime() + "time": self.getPrintTime() } self._callback.mcPrintjobDone() self._changeState(self.STATE_OPERATIONAL) From b8a59097c270740a9a96a2145998436796e04e5c Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Fri, 14 Feb 2014 22:51:27 -0800 Subject: [PATCH 06/18] correct response code for connection success --- docs/api/connection.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/connection.rst b/docs/api/connection.rst index 27feb765..19841545 100644 --- a/docs/api/connection.rst +++ b/docs/api/connection.rst @@ -114,6 +114,6 @@ Issue a connection command Defaults to ``false`` if not set. :json boolean autoconnect: ``connect`` command: Whether to attempt to automatically connect to the printer on server startup. If not set no changes will be made to the current setting. - :statuscode 200: No error + :statuscode 204: No error :statuscode 400: If the selected `port` or `baudrate` for a ``connect`` command are not part of the available options. \ No newline at end of file From 322834b762548ccedab1dbc2c2704dc62b5db0a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sat, 15 Feb 2014 09:58:52 +0100 Subject: [PATCH 07/18] Moved "Less scary error message" patch to new location --- src/octoprint/server/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 1fccc3e3..705c1afb 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -190,6 +190,8 @@ class Server(): printer.connect(port, baudrate) try: IOLoop.instance().start() + except KeyboardInterrupt: + logger.info("Goodbye!") except: logger.fatal("Now that is embarrassing... Something really really went wrong here. Please report this including the stacktrace below in OctoPrint's bugtracker. Thanks!") logger.exception("Stacktrace follows:") From 3f86272913ffe662b4eb2296c6b01be6d9e1f81f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sat, 15 Feb 2014 10:00:14 +0100 Subject: [PATCH 08/18] There's now a different mechanism in place to prevent mobile browsers from crashing on large gcode files being visualized --- src/octoprint/static/js/app/main.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/octoprint/static/js/app/main.js b/src/octoprint/static/js/app/main.js index 66fd1ba4..dfe612c1 100644 --- a/src/octoprint/static/js/app/main.js +++ b/src/octoprint/static/js/app/main.js @@ -1,10 +1,4 @@ $(function() { - //Detect mobile browsers and remove gcode pane - //from http://stackoverflow.com/questions/3514784/what-is-the-best-way-to-detect-a-handheld-device-in-jquery - if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { - $("#gcode, a[href='#gcode']").remove(); - } - //~~ Initialize view models var loginStateViewModel = new LoginStateViewModel(); var usersViewModel = new UsersViewModel(loginStateViewModel); From 592f3dce9a4e808c3e0c3287af89093a4454d55d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sat, 15 Feb 2014 10:26:39 +0100 Subject: [PATCH 09/18] Bugfix: Do not run gcode analyzer when a print is ongoing Evaluate active flag on gcode analyzer AFTER fetching an item from the work queue, otherwise it will always start working once it finds something if the active flag was true once but then switched to false while the queue was still empty. Thanks to @Salandora for spotting this. Fixes #357 --- src/octoprint/gcodefiles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/octoprint/gcodefiles.py b/src/octoprint/gcodefiles.py index 34860bad..a6cf1fe4 100644 --- a/src/octoprint/gcodefiles.py +++ b/src/octoprint/gcodefiles.py @@ -560,8 +560,6 @@ class MetadataAnalyzer: def _work(self): aborted = None while True: - self._active.wait() - if aborted is not None: filename = aborted aborted = None @@ -570,6 +568,8 @@ class MetadataAnalyzer: (priority, filename) = self._queue.get() self._logger.debug("Processing file %s from queue (priority %d)" % (filename, priority)) + self._active.wait() + try: self._analyzeGcode(filename) self._queue.task_done() From b115b6f66ca80c2c38e1aa214e278416ea3cda0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 17 Feb 2014 10:25:40 +0100 Subject: [PATCH 10/18] Only list those SD files that have a ASCII filename According to the RepRap protocol we should actually not ever get anything other than those anyways, as the protocol is defined as ASCII-only. In the future there might be a way to somehow handle such files too, for the time being this fixes issues though where non-ascii files on the SD made the whole SD file handling not work. Closes #381 --- src/octoprint/util/__init__.py | 18 ++++++++++++++++++ src/octoprint/util/comm.py | 10 +++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/octoprint/util/__init__.py b/src/octoprint/util/__init__.py index 437af118..0c5e66d2 100644 --- a/src/octoprint/util/__init__.py +++ b/src/octoprint/util/__init__.py @@ -191,6 +191,24 @@ def silentRemove(file): pass +def sanitizeAscii(line): + return unicode(line, 'ascii', 'replace').encode('ascii', 'replace').rstrip() + + +def filterNonAscii(line): + """ + Returns True if the line contains non-ascii characters, false otherwise + + @param line the line to test + """ + + try: + unicode(line, 'ascii').encode('ascii') + return False + except ValueError: + return True + + def getJsonCommandFromRequest(request, valid_commands): if not "application/json" in request.headers["Content-Type"]: return None, None, make_response("Expected content-type JSON", 400) diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 6a805d7e..091bbddf 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -23,7 +23,7 @@ from octoprint.settings import settings from octoprint.events import eventManager, Events from octoprint.filemanager.destinations import FileDestinations from octoprint.gcodefiles import isGcodeFileName -from octoprint.util import getExceptionString, getNewTimeout +from octoprint.util import getExceptionString, getNewTimeout, sanitizeAscii, filterNonAscii from octoprint.util.virtual import VirtualPrinter try: @@ -605,7 +605,11 @@ class MachineCom(object): ##~~ SD file list # if we are currently receiving an sd file list, each line is just a filename, so just read it and abort processing if self._sdFileList and isGcodeFileName(line.strip().lower()) and not 'End file list' in line: - self._sdFiles.append(line.strip().lower()) + filename = line.strip().lower() + if filterNonAscii(filename): + self._logger.warn("Got a file from printer's SD that has a non-ascii filename (%s), that shouldn't happen according to the protocol" % filename) + else: + self._sdFiles.append(filename) continue ##~~ Temperature processing @@ -920,7 +924,7 @@ class MachineCom(object): if ret == '': #self._log("Recv: TIMEOUT") return '' - self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip())) + self._log("Recv: %s" % sanitizeAscii(ret)) return ret def _sendNext(self): From 036e0af960fc85a4867b361a87970a84c7bcfca0 Mon Sep 17 00:00:00 2001 From: Chris Kosmakos Date: Thu, 20 Feb 2014 10:46:22 +0000 Subject: [PATCH 11/18] added basic terminal command history use up/down keys to navigate through command history. --- .../static/js/app/viewmodels/terminal.js | 16 +++++++++++++++- src/octoprint/templates/index.jinja2 | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/octoprint/static/js/app/viewmodels/terminal.js b/src/octoprint/static/js/app/viewmodels/terminal.js index d739ea0e..076ae607 100644 --- a/src/octoprint/static/js/app/viewmodels/terminal.js +++ b/src/octoprint/static/js/app/viewmodels/terminal.js @@ -21,6 +21,10 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) { self.filters = self.settings.terminalFilters; self.filterRegex = undefined; + self.cmdHistory = []; + self.cmdHistoryCnt = 0; + self.cmdHistoryIdx = -1; + self.activeFilters = ko.observableArray([]); self.activeFilters.subscribe(function(e) { self.updateFilterRegex(); @@ -105,13 +109,23 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) { contentType: "application/json; charset=UTF-8", data: JSON.stringify({"command": command}) }); + self.cmdHistory[self.cmdHistoryCnt++] = command + self.cmdHistoryIdx = self.cmdHistoryCnt self.command(""); } } - self.handleEnter = function(event) { + self.handleKeys = function(event) { if (event.keyCode == 13) { self.sendCommand(); + } else if (event.keyCode == 38) { + if (self.cmdHistoryIdx >= 0) { + self.command(self.cmdHistory[--self.cmdHistoryIdx]) + } + } else if (event.keyCode == 40) { + if (self.cmdHistoryIdx < self.cmdHistoryCnt) { + self.command(self.cmdHistory[++self.cmdHistoryIdx]) + } } } diff --git a/src/octoprint/templates/index.jinja2 b/src/octoprint/templates/index.jinja2 index 17f25b44..f74ec4ab 100644 --- a/src/octoprint/templates/index.jinja2 +++ b/src/octoprint/templates/index.jinja2 @@ -516,7 +516,7 @@ From cf25d3d7be8884b12d17d00a56787c196cc6865d Mon Sep 17 00:00:00 2001 From: Salandora Date: Thu, 20 Feb 2014 18:08:21 +0100 Subject: [PATCH 12/18] Moved Log tab to Settings --- src/octoprint/static/js/app/main.js | 6 ++++ src/octoprint/templates/index.jinja2 | 35 ------------------ src/octoprint/templates/settings.jinja2 | 47 +++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 35 deletions(-) diff --git a/src/octoprint/static/js/app/main.js b/src/octoprint/static/js/app/main.js index 4c6b934c..68a47449 100644 --- a/src/octoprint/static/js/app/main.js +++ b/src/octoprint/static/js/app/main.js @@ -289,6 +289,12 @@ $(function() { } } + ko.bindingHandlers.allowBindings = { + init: function (elem, valueAccessor) { + return { controlsDescendantBindings: !valueAccessor() }; + } + }; + ko.applyBindings(connectionViewModel, document.getElementById("connection_accordion")); ko.applyBindings(printerStateViewModel, document.getElementById("state_accordion")); ko.applyBindings(gcodeFilesViewModel, document.getElementById("files_accordion")); diff --git a/src/octoprint/templates/index.jinja2 b/src/octoprint/templates/index.jinja2 index cae0178a..9932458f 100644 --- a/src/octoprint/templates/index.jinja2 +++ b/src/octoprint/templates/index.jinja2 @@ -253,7 +253,6 @@ {% if enableGCodeVisualizer %}
  • GCode Viewer
  • {% endif %}
  • Terminal
  • {% if enableTimelapse %}
  • Timelapse
  • {% endif %} -
  • Logs
  • @@ -587,40 +586,6 @@
    {% endif %} -
    -

    Logs

    - - - - - - - - - - - - - - - - - -
    NameSizeAction
     | 
    - -
    diff --git a/src/octoprint/templates/settings.jinja2 b/src/octoprint/templates/settings.jinja2 index a2f8e2ac..5437d55a 100644 --- a/src/octoprint/templates/settings.jinja2 +++ b/src/octoprint/templates/settings.jinja2 @@ -20,6 +20,7 @@
  • Folders
  • Appearance
  • +
  • Logs
  • @@ -535,6 +536,52 @@
    {% endif %} +
    +
    +

    Logs

    + + + + + + + + + + + + + + + + + +
    NameSizeAction
    +  |  +
    + +
    +
    From 197d46b511fec106e400f1e6535bce161950cd43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sun, 23 Feb 2014 11:17:29 +0100 Subject: [PATCH 13/18] Fixed indentation --- src/octoprint/static/js/app/viewmodels/log.js | 8 +- src/octoprint/templates/settings.jinja2 | 92 +++++++++---------- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/src/octoprint/static/js/app/viewmodels/log.js b/src/octoprint/static/js/app/viewmodels/log.js index 15578885..1f8af2d5 100644 --- a/src/octoprint/static/js/app/viewmodels/log.js +++ b/src/octoprint/static/js/app/viewmodels/log.js @@ -44,11 +44,11 @@ function LogViewModel(loginStateViewModel) { }; self.fromResponse = function(response) { - var files = response.files; - if (files === undefined) - return; + var files = response.files; + if (files === undefined) + return; - self.listHelper.updateItems(files); + self.listHelper.updateItems(files); } self.removeFile = function(filename) { diff --git a/src/octoprint/templates/settings.jinja2 b/src/octoprint/templates/settings.jinja2 index 5437d55a..340a12e7 100644 --- a/src/octoprint/templates/settings.jinja2 +++ b/src/octoprint/templates/settings.jinja2 @@ -20,7 +20,7 @@
  • Folders
  • Appearance
  • -
  • Logs
  • +
  • Logs
  • @@ -536,52 +536,52 @@
    {% endif %} -
    -
    -

    Logs

    +
    +
    +

    Logs

    - - - - - - - - - - - - - - - - -
    NameSizeAction
    -  |  -
    - -
    -
    + + + + + + + + + + + + + + + + +
    NameSizeAction
    +  |  +
    + +
    +
    From 4a6a458cdd5a2280f118c2cf094d768310af7678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sun, 23 Feb 2014 12:25:46 +0100 Subject: [PATCH 14/18] Removed unused parts from backend code, moved formatting to frontend, added date column, switched to modification date (might be more interesting in case of the log files) --- src/octoprint/server/api/log.py | 26 +++++---------- src/octoprint/static/css/octoprint.css | 2 +- src/octoprint/static/js/app/viewmodels/log.js | 2 +- src/octoprint/static/less/octoprint.less | 32 +++++++++++++++++++ src/octoprint/templates/settings.jinja2 | 16 ++++++---- 5 files changed, 51 insertions(+), 27 deletions(-) diff --git a/src/octoprint/server/api/log.py b/src/octoprint/server/api/log.py index 0fda12dc..c2699628 100644 --- a/src/octoprint/server/api/log.py +++ b/src/octoprint/server/api/log.py @@ -3,31 +3,22 @@ __author__ = "Marc Hannappel Salandora" __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' import os -import datetime from flask import request, jsonify, make_response, url_for from werkzeug.utils import secure_filename -import octoprint.util as util -from octoprint.settings import settings, valid_boolean_trues +from octoprint.settings import settings -from octoprint.server import restricted_access, admin_permission +from octoprint.server import restricted_access from octoprint.server.util import redirectToTornado from octoprint.server.api import api + @api.route("/logs", methods=["GET"]) def getLogData(): files = _getLogFiles() return jsonify(files=files) -@api.route("/logs/online/", methods=["GET"]) -def onlineLog(filename): - secure = os.path.join(settings().getBaseFolder("logs"), secure_filename(filename)) - if not os.path.exists(secure): - return make_response("Unknown filename: %s" % filename, 404) - - file = open(secure, 'r') - return jsonify(data=file.read()) @api.route("/logs/", methods=["GET"]) def downloadLog(filename): @@ -39,10 +30,11 @@ def downloadLog(filename): def deleteLog(filename): secure = os.path.join(settings().getBaseFolder("logs"), secure_filename(filename)) if os.path.exists(secure): - os.remove(secure) + os.remove(secure) return getLogData() + def _getLogFiles(): files = [] basedir = settings().getBaseFolder("logs") @@ -50,13 +42,11 @@ def _getLogFiles(): statResult = os.stat(os.path.join(basedir, osFile)) files.append({ "name": osFile, - "size": util.getFormattedSize(statResult.st_size), + "size": statResult.st_size, "bytes": statResult.st_size, - "date": util.getFormattedDateTime(datetime.datetime.fromtimestamp(statResult.st_ctime)) + "date": int(statResult.st_mtime), + "url": url_for("index") + "downloads/logs/" + osFile }) - for file in files: - file["url"] = url_for("index") + "downloads/logs/" + file["name"] - return files diff --git a/src/octoprint/static/css/octoprint.css b/src/octoprint/static/css/octoprint.css index 6bdb45f9..524090a1 100644 --- a/src/octoprint/static/css/octoprint.css +++ b/src/octoprint/static/css/octoprint.css @@ -1 +1 @@ -.btn{display:inline-block;*display:inline;*zoom:1;padding:4px 12px;margin-bottom:0;font-size:14px;line-height:20px;text-align:center;vertical-align:middle;cursor:pointer;color:#333;text-shadow:0 1px 1px rgba(255,255,255,0.75);background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#e6e6e6;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border:1px solid #ccc;*border:0;border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*margin-left:.3em;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05)}.btn:hover,.btn:focus,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover,.btn:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.btn.disabled,.btn[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:11px 19px;font-size:17.5px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.btn-large [class^="icon-"],.btn-large [class*=" icon-"]{margin-top:4px}.btn-small{padding:2px 10px;font-size:11.9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-small [class^="icon-"],.btn-small [class*=" icon-"]{margin-top:0}.btn-mini [class^="icon-"],.btn-mini [class*=" icon-"]{margin-top:-1px}.btn-mini{padding:0 6px;font-size:10.5px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#04c;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.btn-primary:active,.btn-primary.active{background-color:#039 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0);border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#f89406;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4f49;background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(to bottom,#ee5f5b,#bd362f);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffbd362f',GradientType=0);border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#bd362f;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb75b;background-image:-moz-linear-gradient(top,#62c462,#51a351);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(to bottom,#62c462,#51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff51a351',GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#51a351;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(to bottom,#5bc0de,#2f96b4);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2f96b4',GradientType=0);border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#2f96b4;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#363636;background-image:-moz-linear-gradient(top,#444,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#444),to(#222));background-image:-webkit-linear-gradient(top,#444,#222);background-image:-o-linear-gradient(top,#444,#222);background-image:linear-gradient(to bottom,#444,#222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444',endColorstr='#ff222222',GradientType=0);border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#222;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-inverse:hover,.btn-inverse:focus,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{border-color:transparent;cursor:pointer;color:#08c;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover,.btn-link:focus{color:#005580;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus{color:#333;text-decoration:none}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}body{padding-top:60px}#navbar .navbar-inner{background-color:#ebebeb;background-image:-moz-linear-gradient(top,#fff,#ccc);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#ccc));background-image:-webkit-linear-gradient(top,#fff,#ccc);background-image:-o-linear-gradient(top,#fff,#ccc);background-image:linear-gradient(to bottom,#fff,#ccc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffcccccc',GradientType=0)}#navbar .navbar-inner .brand,#navbar .navbar-inner .nav>li>a{text-shadow:0 1px 0 #fff;color:#333}#navbar .navbar-inner .brand .caret,#navbar .navbar-inner .nav>li>a .caret{border-bottom-color:#939393;border-top-color:#939393}#navbar .navbar-inner .brand:hover .caret,#navbar .navbar-inner .nav>li>a:hover .caret,#navbar .navbar-inner .brand:focus .caret,#navbar .navbar-inner .nav>li>a:focus .caret{border-bottom-color:#636363;border-top-color:#636363}#navbar .navbar-inner .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner .nav li.dropdown.open.active>.dropdown-toggle{background-color:#e0e0e0;background-image:-moz-linear-gradient(top,#ccc,#fff);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ccc),to(#fff));background-image:-webkit-linear-gradient(top,#ccc,#fff);background-image:-o-linear-gradient(top,#ccc,#fff);background-image:linear-gradient(to bottom,#ccc,#fff);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffcccccc',endColorstr='#ffffffff',GradientType=0)}#navbar .navbar-inner.red{background-color:#e36565;background-image:-moz-linear-gradient(top,#f9a0a0,#c20c0c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f9a0a0),to(#c20c0c));background-image:-webkit-linear-gradient(top,#f9a0a0,#c20c0c);background-image:-o-linear-gradient(top,#f9a0a0,#c20c0c);background-image:linear-gradient(to bottom,#f9a0a0,#c20c0c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9a0a0',endColorstr='#ffc20c0c',GradientType=0)}#navbar .navbar-inner.red .brand,#navbar .navbar-inner.red .nav>li>a{text-shadow:0 1px 0 #c20c0c;color:#f2f2f2}#navbar .navbar-inner.red .brand .caret,#navbar .navbar-inner.red .nav>li>a .caret{border-bottom-color:#f28d8d;border-top-color:#f28d8d}#navbar .navbar-inner.red .brand:hover .caret,#navbar .navbar-inner.red .nav>li>a:hover .caret,#navbar .navbar-inner.red .brand:focus .caret,#navbar .navbar-inner.red .nav>li>a:focus .caret{border-bottom-color:#f2c0c0;border-top-color:#f2c0c0}#navbar .navbar-inner.red .brand span{background-image:url(../img/tentacle-20x20-light.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.red .brand span{background-image:url(../img/tentacle-20x20-light@2x.png)}}#navbar .navbar-inner.red .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.red .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.red .nav li.dropdown.open.active>.dropdown-toggle{background-color:#d84747;background-image:-moz-linear-gradient(top,#c20c0c,#f9a0a0);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c20c0c),to(#f9a0a0));background-image:-webkit-linear-gradient(top,#c20c0c,#f9a0a0);background-image:-o-linear-gradient(top,#c20c0c,#f9a0a0);background-image:linear-gradient(to bottom,#c20c0c,#f9a0a0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc20c0c',endColorstr='#fff9a0a0',GradientType=0)}#navbar .navbar-inner.orange{background-color:#e39665;background-image:-moz-linear-gradient(top,#f9c3a0,#c2530c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f9c3a0),to(#c2530c));background-image:-webkit-linear-gradient(top,#f9c3a0,#c2530c);background-image:-o-linear-gradient(top,#f9c3a0,#c2530c);background-image:linear-gradient(to bottom,#f9c3a0,#c2530c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9c3a0',endColorstr='#ffc2530c',GradientType=0)}#navbar .navbar-inner.orange .brand,#navbar .navbar-inner.orange .nav>li>a{text-shadow:0 1px 0 #f6a570;color:#333}#navbar .navbar-inner.orange .brand .caret,#navbar .navbar-inner.orange .nav>li>a .caret{border-bottom-color:#93552e;border-top-color:#93552e}#navbar .navbar-inner.orange .brand:hover .caret,#navbar .navbar-inner.orange .nav>li>a:hover .caret,#navbar .navbar-inner.orange .brand:focus .caret,#navbar .navbar-inner.orange .nav>li>a:focus .caret{border-bottom-color:#634430;border-top-color:#634430}#navbar .navbar-inner.orange .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.orange .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner.orange .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.orange .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.orange .nav li.dropdown.open.active>.dropdown-toggle{background-color:#d88047;background-image:-moz-linear-gradient(top,#c2530c,#f9c3a0);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c2530c),to(#f9c3a0));background-image:-webkit-linear-gradient(top,#c2530c,#f9c3a0);background-image:-o-linear-gradient(top,#c2530c,#f9c3a0);background-image:linear-gradient(to bottom,#c2530c,#f9c3a0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc2530c',endColorstr='#fff9c3a0',GradientType=0)}#navbar .navbar-inner.yellow{background-color:#e3d765;background-image:-moz-linear-gradient(top,#f9f0a0,#c2b00c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f9f0a0),to(#c2b00c));background-image:-webkit-linear-gradient(top,#f9f0a0,#c2b00c);background-image:-o-linear-gradient(top,#f9f0a0,#c2b00c);background-image:linear-gradient(to bottom,#f9f0a0,#c2b00c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f0a0',endColorstr='#ffc2b00c',GradientType=0)}#navbar .navbar-inner.yellow .brand,#navbar .navbar-inner.yellow .nav>li>a{text-shadow:0 1px 0 #f6e970;color:#333}#navbar .navbar-inner.yellow .brand .caret,#navbar .navbar-inner.yellow .nav>li>a .caret{border-bottom-color:#93892e;border-top-color:#93892e}#navbar .navbar-inner.yellow .brand:hover .caret,#navbar .navbar-inner.yellow .nav>li>a:hover .caret,#navbar .navbar-inner.yellow .brand:focus .caret,#navbar .navbar-inner.yellow .nav>li>a:focus .caret{border-bottom-color:#635e30;border-top-color:#635e30}#navbar .navbar-inner.yellow .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.yellow .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner.yellow .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.yellow .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.yellow .nav li.dropdown.open.active>.dropdown-toggle{background-color:#d8ca47;background-image:-moz-linear-gradient(top,#c2b00c,#f9f0a0);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c2b00c),to(#f9f0a0));background-image:-webkit-linear-gradient(top,#c2b00c,#f9f0a0);background-image:-o-linear-gradient(top,#c2b00c,#f9f0a0);background-image:linear-gradient(to bottom,#c2b00c,#f9f0a0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc2b00c',endColorstr='#fff9f0a0',GradientType=0)}#navbar .navbar-inner.green{background-color:#98f064;background-image:-moz-linear-gradient(top,#c8ffa7,#50da00);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c8ffa7),to(#50da00));background-image:-webkit-linear-gradient(top,#c8ffa7,#50da00);background-image:-o-linear-gradient(top,#c8ffa7,#50da00);background-image:linear-gradient(to bottom,#c8ffa7,#50da00);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc8ffa7',endColorstr='#ff50da00',GradientType=0)}#navbar .navbar-inner.green .brand,#navbar .navbar-inner.green .nav>li>a{text-shadow:0 1px 0 #a7ff74;color:#333}#navbar .navbar-inner.green .brand .caret,#navbar .navbar-inner.green .nav>li>a .caret{border-bottom-color:#55992e;border-top-color:#55992e}#navbar .navbar-inner.green .brand:hover .caret,#navbar .navbar-inner.green .nav>li>a:hover .caret,#navbar .navbar-inner.green .brand:focus .caret,#navbar .navbar-inner.green .nav>li>a:focus .caret{border-bottom-color:#446630;border-top-color:#446630}#navbar .navbar-inner.green .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.green .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner.green .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.green .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.green .nav li.dropdown.open.active>.dropdown-toggle{background-color:#80e943;background-image:-moz-linear-gradient(top,#50da00,#c8ffa7);background-image:-webkit-gradient(linear,0 0,0 100%,from(#50da00),to(#c8ffa7));background-image:-webkit-linear-gradient(top,#50da00,#c8ffa7);background-image:-o-linear-gradient(top,#50da00,#c8ffa7);background-image:linear-gradient(to bottom,#50da00,#c8ffa7);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff50da00',endColorstr='#ffc8ffa7',GradientType=0)}#navbar .navbar-inner.blue{background-color:#6498f0;background-image:-moz-linear-gradient(top,#a7c8ff,#0050da);background-image:-webkit-gradient(linear,0 0,0 100%,from(#a7c8ff),to(#0050da));background-image:-webkit-linear-gradient(top,#a7c8ff,#0050da);background-image:-o-linear-gradient(top,#a7c8ff,#0050da);background-image:linear-gradient(to bottom,#a7c8ff,#0050da);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffa7c8ff',endColorstr='#ff0050da',GradientType=0)}#navbar .navbar-inner.blue .brand,#navbar .navbar-inner.blue .nav>li>a{text-shadow:0 1px 0 #74a7ff;color:#333}#navbar .navbar-inner.blue .brand .caret,#navbar .navbar-inner.blue .nav>li>a .caret{border-bottom-color:#2e5599;border-top-color:#2e5599}#navbar .navbar-inner.blue .brand:hover .caret,#navbar .navbar-inner.blue .nav>li>a:hover .caret,#navbar .navbar-inner.blue .brand:focus .caret,#navbar .navbar-inner.blue .nav>li>a:focus .caret{border-bottom-color:#304466;border-top-color:#304466}#navbar .navbar-inner.blue .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.blue .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner.blue .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.blue .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.blue .nav li.dropdown.open.active>.dropdown-toggle{background-color:#4380e9;background-image:-moz-linear-gradient(top,#0050da,#a7c8ff);background-image:-webkit-gradient(linear,0 0,0 100%,from(#0050da),to(#a7c8ff));background-image:-webkit-linear-gradient(top,#0050da,#a7c8ff);background-image:-o-linear-gradient(top,#0050da,#a7c8ff);background-image:linear-gradient(to bottom,#0050da,#a7c8ff);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0050da',endColorstr='#ffa7c8ff',GradientType=0)}#navbar .navbar-inner.violet{background-color:#9864f0;background-image:-moz-linear-gradient(top,#c8a7ff,#5000da);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c8a7ff),to(#5000da));background-image:-webkit-linear-gradient(top,#c8a7ff,#5000da);background-image:-o-linear-gradient(top,#c8a7ff,#5000da);background-image:linear-gradient(to bottom,#c8a7ff,#5000da);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc8a7ff',endColorstr='#ff5000da',GradientType=0)}#navbar .navbar-inner.violet .brand,#navbar .navbar-inner.violet .nav>li>a{text-shadow:0 1px 0 #5000da;color:#f2f2f2}#navbar .navbar-inner.violet .brand .caret,#navbar .navbar-inner.violet .nav>li>a .caret{border-bottom-color:#b58df9;border-top-color:#b58df9}#navbar .navbar-inner.violet .brand:hover .caret,#navbar .navbar-inner.violet .nav>li>a:hover .caret,#navbar .navbar-inner.violet .brand:focus .caret,#navbar .navbar-inner.violet .nav>li>a:focus .caret{border-bottom-color:#d3c0f5;border-top-color:#d3c0f5}#navbar .navbar-inner.violet .brand span{background-image:url(../img/tentacle-20x20-light.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.violet .brand span{background-image:url(../img/tentacle-20x20-light@2x.png)}}#navbar .navbar-inner.violet .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.violet .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.violet .nav li.dropdown.open.active>.dropdown-toggle{background-color:#8043e9;background-image:-moz-linear-gradient(top,#5000da,#c8a7ff);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5000da),to(#c8a7ff));background-image:-webkit-linear-gradient(top,#5000da,#c8a7ff);background-image:-o-linear-gradient(top,#5000da,#c8a7ff);background-image:linear-gradient(to bottom,#5000da,#c8a7ff);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5000da',endColorstr='#ffc8a7ff',GradientType=0)}#navbar .navbar-inner.black{background-color:#4f4f4f;background-image:-moz-linear-gradient(top,#787878,#121212);background-image:-webkit-gradient(linear,0 0,0 100%,from(#787878),to(#121212));background-image:-webkit-linear-gradient(top,#787878,#121212);background-image:-o-linear-gradient(top,#787878,#121212);background-image:linear-gradient(to bottom,#787878,#121212);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff787878',endColorstr='#ff121212',GradientType=0)}#navbar .navbar-inner.black .brand,#navbar .navbar-inner.black .nav>li>a{text-shadow:0 1px 0 #121212;color:#f2f2f2}#navbar .navbar-inner.black .brand .caret,#navbar .navbar-inner.black .nav>li>a .caret{border-bottom-color:#959595;border-top-color:#959595}#navbar .navbar-inner.black .brand:hover .caret,#navbar .navbar-inner.black .nav>li>a:hover .caret,#navbar .navbar-inner.black .brand:focus .caret,#navbar .navbar-inner.black .nav>li>a:focus .caret{border-bottom-color:#c4c4c4;border-top-color:#c4c4c4}#navbar .navbar-inner.black .brand span{background-image:url(../img/tentacle-20x20-light.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.black .brand span{background-image:url(../img/tentacle-20x20-light@2x.png)}}#navbar .navbar-inner.black .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.black .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.black .nav li.dropdown.open.active>.dropdown-toggle{background-color:#3b3b3b;background-image:-moz-linear-gradient(top,#121212,#787878);background-image:-webkit-gradient(linear,0 0,0 100%,from(#121212),to(#787878));background-image:-webkit-linear-gradient(top,#121212,#787878);background-image:-o-linear-gradient(top,#121212,#787878);background-image:linear-gradient(to bottom,#121212,#787878);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff121212',endColorstr='#ff787878',GradientType=0)}#navbar .navbar-inner .brand span{background-size:20px 20px;background-position:left center;padding-left:24px;background-repeat:no-repeat}.octoprint-container .tab-content{padding:9px 15px;border-left:1px solid #DDD;border-right:1px solid #DDD;border-bottom:1px solid #DDD;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px}.octoprint-container .nav{margin-bottom:0}.octoprint-container .tab-content h1{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333;border:0;border-bottom:1px solid #e5e5e5;font-weight:normal}.octoprint-container .accordion-heading .accordion-heading-button{float:right}.octoprint-container .accordion-heading .accordion-heading-button a{display:inline-block;padding:8px 15px;font-size:14px;line-height:20px;color:#000;text-decoration:none;background:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.octoprint-container .accordion-heading a.accordion-toggle{display:inline-block}.octoprint-container .accordion-heading [class^="icon-"],.octoprint-container .accordion-heading [class*=" icon-"]{color:#000}.print-control .btn{padding-left:4px;padding-right:4px}.upload-buttons .btn{margin-right:0}table{table-layout:fixed}table .popover-title{text-overflow:ellipsis;word-break:break-all}table th,table td{overflow:hidden}table th.gcode_files_name,table td.gcode_files_name{text-overflow:ellipsis;white-space:nowrap;text-align:left}table th.gcode_files_size,table td.gcode_files_size{text-align:right;width:55px}table th.gcode_files_action,table td.gcode_files_action{text-align:center;width:70px}table th.gcode_files_action a,table td.gcode_files_action a{text-decoration:none;color:#000}table th.gcode_files_action a.disabled,table td.gcode_files_action a.disabled{color:#ccc;cursor:default}table th.timelapse_files_name,table td.timelapse_files_name{text-overflow:ellipsis;text-align:left}table th.timelapse_files_size,table td.timelapse_files_size{text-align:right;width:55px}table th.timelapse_files_action,table td.timelapse_files_action{text-align:center;width:45px}table th.timelapse_files_action a,table td.timelapse_files_action a{text-decoration:none;color:#000}table th.timelapse_files_action a.disabled,table td.timelapse_files_action a.disabled{color:#ccc;cursor:default}table th.settings_users_name,table td.settings_users_name{text-overflow:ellipsis;text-align:left}table th.settings_users_active,table td.settings_users_active,table th.settings_users_admin,table td.settings_users_admin{text-align:center;width:55px}table th.settings_users_actions,table td.settings_users_actions{text-align:center;width:60px}table th.settings_users_actions a,table td.settings_users_actions a{text-decoration:none;color:#000}table th.settings_users_actions a.disabled,table td.settings_users_actions a.disabled{color:#ccc;cursor:default}#temperature-graph{height:350px;width:100%;background-image:url("../img/graph-background.png");background-position:center;background-repeat:no-repeat}.tab-content,.tab-pane{overflow:visible}.tempInput{width:50px}#temp_newTemp,#temp_newBedTemp,#speed_innerWall,#speed_outerWall,#speed_fill,#speed_support,#webcam_timelapse_interval{text-align:right}ul.dropdown-menu li a{cursor:pointer}#connection_ports,#connection_baudrates{width:100%}#offline_overlay{position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;display:none}#offline_overlay_background{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#000;filter:alpha(opacity=50);-moz-opacity:.5;-khtml-opacity:.5;opacity:.5}#offline_overlay_wrapper{position:absolute;top:0;bottom:0;left:0;right:0;padding-top:60px}#offline_overlay_wrapper .container{margin:auto}#webcam_container{width:100%}#webcam_container .flipH{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1)}#webcam_container .flipV{-webkit-transform:scaleY(-1);-moz-transform:scaleY(-1)}#webcam_container .flipH.flipV{-webkit-transform:scaleX(-1) scaleY(-1);-moz-transform:scaleX(-1) scaleY(-1)}#files .popover{font-size:85%}#files .popover p:last-child{margin-bottom:0}#files table{margin-bottom:0}#control{overflow:hidden}#control .jog-panel{float:left;margin-right:19px}#control h1{text-align:left}#control .jog-panel>div{text-align:center}#control .jog-panel>div.distance{text-align:left}#control .box{width:30px;height:30px;margin-right:10px;margin-bottom:10px;padding-left:8px}#control .control-box{display:block;height:30px;margin-bottom:10px}#control .btn-group{margin-bottom:10px}#control .btn-group.distance>.btn{width:43px;padding:3px 0;height:30px}#gcode .progress{width:582px}#gcode .progress .bar{-webkit-transition:width 0s linear;-moz-transition:width 0s linear;-o-transition:width 0s linear;transition:width 0s linear}#gcode #gcode_layer_slider{height:568px;float:right}#gcode #gcode_layer_slider .slider-handle{width:14px;height:14px;margin-left:-3px;margin-top:-7px}#gcode #gcode_command_slider .slider-handle{width:14px;height:14px;margin-left:-7px;margin-top:-3px}#term #terminal-output{min-height:340px}.footer ul{margin:0}.footer ul li{display:inline;margin-left:1em;font-size:85%}.footer ul li:first-child{margin-left:0}.footer ul li a{color:#555}.text-right{text-align:right}.overflow_visible{overflow:visible!important}#drop_overlay{position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;display:none}#drop_overlay.in{display:block}#drop_overlay #drop_overlay_background{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#000;filter:alpha(opacity=50);-moz-opacity:.5;-khtml-opacity:.5;opacity:.5}#drop_overlay #drop_overlay_wrapper{position:absolute;top:0;bottom:0;left:0;right:0;padding-top:60px}#drop_overlay #drop_overlay_wrapper #drop,#drop_overlay #drop_overlay_wrapper #drop_background{position:absolute;top:50%;left:50%;margin-left:-200px;margin-top:-200px}#drop_overlay #drop_overlay_wrapper #drop_locally,#drop_overlay #drop_overlay_wrapper #drop_locally_background{position:absolute;top:50%;left:50%;margin-left:-425px;margin-top:-200px}#drop_overlay #drop_overlay_wrapper #drop_sd,#drop_overlay #drop_overlay_wrapper #drop_sd_background{position:absolute;top:50%;left:50%;margin-left:25px;margin-top:-200px}#drop_overlay #drop_overlay_wrapper .dropzone{width:404px;height:404px;z-index:10001;color:#fff;font-size:30px}#drop_overlay #drop_overlay_wrapper .dropzone i{font-size:50px}#drop_overlay #drop_overlay_wrapper .dropzone .centered{display:table-cell;text-align:center;vertical-align:middle;width:400px;height:400px;line-height:40px;filter:alpha(opacity=100);-moz-opacity:1.0;-khtml-opacity:1.0;opacity:1.0}#drop_overlay #drop_overlay_wrapper .dropzone_background{width:400px;height:400px;border:2px dashed #eee;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;background-color:#000;filter:alpha(opacity=25);-moz-opacity:.25;-khtml-opacity:.25;opacity:.25}#drop_overlay #drop_overlay_wrapper .dropzone_background.hover{background-color:#000;filter:alpha(opacity=50);-moz-opacity:.5;-khtml-opacity:.5;opacity:.5}#drop_overlay #drop_overlay_wrapper .dropzone_background.fade{-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-ms-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out;opacity:1}.icon-sd-black-14{background:url("../img/icon-sd-black-14.png") 0 3px no-repeat;width:11px;height:17px}.slider .slider-selection{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#04c;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.slider .slider-selection:hover,.slider .slider-selection:focus,.slider .slider-selection:active,.slider .slider-selection.active,.slider .slider-selection.disabled,.slider .slider-selection[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.slider .slider-selection:active,.slider .slider-selection.active{background-color:#039 \9}.slider.slider-disabled .slider-selection{background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.slider .slider-track{background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.slider.slider-disabled .slider-track{background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.slider .slider-handle{display:inline-block;*display:inline;*zoom:1;padding:4px 12px;font-size:14px;line-height:20px;text-align:center;vertical-align:middle;cursor:pointer;color:#333;text-shadow:0 1px 1px rgba(255,255,255,0.75);background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#e6e6e6;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border:1px solid #ccc;*border:0;border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*margin-left:.3em;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);padding:0;margin-bottom:0;opacity:1;filter:alpha(opacity=100)}.slider .slider-handle:hover,.slider .slider-handle:focus,.slider .slider-handle:active,.slider .slider-handle.active,.slider .slider-handle.disabled,.slider .slider-handle[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.slider .slider-handle:active,.slider .slider-handle.active{background-color:#ccc \9}.slider .slider-handle:first-child{*margin-left:0}.slider .slider-handle:hover,.slider .slider-handle:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.slider .slider-handle:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.slider .slider-handle.active,.slider .slider-handle:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.slider .slider-handle.disabled,.slider .slider-handle[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.slider .slider-handle.hide{display:none}.slider .slider-handle.round{-webkit-border-radius:50%;-moz-border-radius:50%;border-radius:50%} \ No newline at end of file +.btn{display:inline-block;*display:inline;*zoom:1;padding:4px 12px;margin-bottom:0;font-size:14px;line-height:20px;text-align:center;vertical-align:middle;cursor:pointer;color:#333;text-shadow:0 1px 1px rgba(255,255,255,0.75);background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#e6e6e6;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border:1px solid #ccc;*border:0;border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*margin-left:.3em;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05)}.btn:hover,.btn:focus,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover,.btn:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.btn.disabled,.btn[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:11px 19px;font-size:17.5px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.btn-large [class^="icon-"],.btn-large [class*=" icon-"]{margin-top:4px}.btn-small{padding:2px 10px;font-size:11.9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-small [class^="icon-"],.btn-small [class*=" icon-"]{margin-top:0}.btn-mini [class^="icon-"],.btn-mini [class*=" icon-"]{margin-top:-1px}.btn-mini{padding:0 6px;font-size:10.5px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#04c;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.btn-primary:active,.btn-primary.active{background-color:#039 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0);border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#f89406;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4f49;background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(to bottom,#ee5f5b,#bd362f);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffbd362f',GradientType=0);border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#bd362f;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb75b;background-image:-moz-linear-gradient(top,#62c462,#51a351);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(to bottom,#62c462,#51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff51a351',GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#51a351;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(to bottom,#5bc0de,#2f96b4);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2f96b4',GradientType=0);border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#2f96b4;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#363636;background-image:-moz-linear-gradient(top,#444,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#444),to(#222));background-image:-webkit-linear-gradient(top,#444,#222);background-image:-o-linear-gradient(top,#444,#222);background-image:linear-gradient(to bottom,#444,#222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444',endColorstr='#ff222222',GradientType=0);border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#222;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.btn-inverse:hover,.btn-inverse:focus,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{border-color:transparent;cursor:pointer;color:#08c;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover,.btn-link:focus{color:#005580;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus{color:#333;text-decoration:none}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}body{padding-top:60px}#navbar .navbar-inner{background-color:#ebebeb;background-image:-moz-linear-gradient(top,#fff,#ccc);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#ccc));background-image:-webkit-linear-gradient(top,#fff,#ccc);background-image:-o-linear-gradient(top,#fff,#ccc);background-image:linear-gradient(to bottom,#fff,#ccc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffcccccc',GradientType=0)}#navbar .navbar-inner .brand,#navbar .navbar-inner .nav>li>a{text-shadow:0 1px 0 #fff;color:#333}#navbar .navbar-inner .brand .caret,#navbar .navbar-inner .nav>li>a .caret{border-bottom-color:#939393;border-top-color:#939393}#navbar .navbar-inner .brand:hover .caret,#navbar .navbar-inner .nav>li>a:hover .caret,#navbar .navbar-inner .brand:focus .caret,#navbar .navbar-inner .nav>li>a:focus .caret{border-bottom-color:#636363;border-top-color:#636363}#navbar .navbar-inner .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner .nav li.dropdown.open.active>.dropdown-toggle{background-color:#e0e0e0;background-image:-moz-linear-gradient(top,#ccc,#fff);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ccc),to(#fff));background-image:-webkit-linear-gradient(top,#ccc,#fff);background-image:-o-linear-gradient(top,#ccc,#fff);background-image:linear-gradient(to bottom,#ccc,#fff);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffcccccc',endColorstr='#ffffffff',GradientType=0)}#navbar .navbar-inner.red{background-color:#e36565;background-image:-moz-linear-gradient(top,#f9a0a0,#c20c0c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f9a0a0),to(#c20c0c));background-image:-webkit-linear-gradient(top,#f9a0a0,#c20c0c);background-image:-o-linear-gradient(top,#f9a0a0,#c20c0c);background-image:linear-gradient(to bottom,#f9a0a0,#c20c0c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9a0a0',endColorstr='#ffc20c0c',GradientType=0)}#navbar .navbar-inner.red .brand,#navbar .navbar-inner.red .nav>li>a{text-shadow:0 1px 0 #c20c0c;color:#f2f2f2}#navbar .navbar-inner.red .brand .caret,#navbar .navbar-inner.red .nav>li>a .caret{border-bottom-color:#f28d8d;border-top-color:#f28d8d}#navbar .navbar-inner.red .brand:hover .caret,#navbar .navbar-inner.red .nav>li>a:hover .caret,#navbar .navbar-inner.red .brand:focus .caret,#navbar .navbar-inner.red .nav>li>a:focus .caret{border-bottom-color:#f2c0c0;border-top-color:#f2c0c0}#navbar .navbar-inner.red .brand span{background-image:url(../img/tentacle-20x20-light.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.red .brand span{background-image:url(../img/tentacle-20x20-light@2x.png)}}#navbar .navbar-inner.red .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.red .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.red .nav li.dropdown.open.active>.dropdown-toggle{background-color:#d84747;background-image:-moz-linear-gradient(top,#c20c0c,#f9a0a0);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c20c0c),to(#f9a0a0));background-image:-webkit-linear-gradient(top,#c20c0c,#f9a0a0);background-image:-o-linear-gradient(top,#c20c0c,#f9a0a0);background-image:linear-gradient(to bottom,#c20c0c,#f9a0a0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc20c0c',endColorstr='#fff9a0a0',GradientType=0)}#navbar .navbar-inner.orange{background-color:#e39665;background-image:-moz-linear-gradient(top,#f9c3a0,#c2530c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f9c3a0),to(#c2530c));background-image:-webkit-linear-gradient(top,#f9c3a0,#c2530c);background-image:-o-linear-gradient(top,#f9c3a0,#c2530c);background-image:linear-gradient(to bottom,#f9c3a0,#c2530c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9c3a0',endColorstr='#ffc2530c',GradientType=0)}#navbar .navbar-inner.orange .brand,#navbar .navbar-inner.orange .nav>li>a{text-shadow:0 1px 0 #f6a570;color:#333}#navbar .navbar-inner.orange .brand .caret,#navbar .navbar-inner.orange .nav>li>a .caret{border-bottom-color:#93552e;border-top-color:#93552e}#navbar .navbar-inner.orange .brand:hover .caret,#navbar .navbar-inner.orange .nav>li>a:hover .caret,#navbar .navbar-inner.orange .brand:focus .caret,#navbar .navbar-inner.orange .nav>li>a:focus .caret{border-bottom-color:#634430;border-top-color:#634430}#navbar .navbar-inner.orange .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.orange .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner.orange .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.orange .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.orange .nav li.dropdown.open.active>.dropdown-toggle{background-color:#d88047;background-image:-moz-linear-gradient(top,#c2530c,#f9c3a0);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c2530c),to(#f9c3a0));background-image:-webkit-linear-gradient(top,#c2530c,#f9c3a0);background-image:-o-linear-gradient(top,#c2530c,#f9c3a0);background-image:linear-gradient(to bottom,#c2530c,#f9c3a0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc2530c',endColorstr='#fff9c3a0',GradientType=0)}#navbar .navbar-inner.yellow{background-color:#e3d765;background-image:-moz-linear-gradient(top,#f9f0a0,#c2b00c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f9f0a0),to(#c2b00c));background-image:-webkit-linear-gradient(top,#f9f0a0,#c2b00c);background-image:-o-linear-gradient(top,#f9f0a0,#c2b00c);background-image:linear-gradient(to bottom,#f9f0a0,#c2b00c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f0a0',endColorstr='#ffc2b00c',GradientType=0)}#navbar .navbar-inner.yellow .brand,#navbar .navbar-inner.yellow .nav>li>a{text-shadow:0 1px 0 #f6e970;color:#333}#navbar .navbar-inner.yellow .brand .caret,#navbar .navbar-inner.yellow .nav>li>a .caret{border-bottom-color:#93892e;border-top-color:#93892e}#navbar .navbar-inner.yellow .brand:hover .caret,#navbar .navbar-inner.yellow .nav>li>a:hover .caret,#navbar .navbar-inner.yellow .brand:focus .caret,#navbar .navbar-inner.yellow .nav>li>a:focus .caret{border-bottom-color:#635e30;border-top-color:#635e30}#navbar .navbar-inner.yellow .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.yellow .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner.yellow .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.yellow .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.yellow .nav li.dropdown.open.active>.dropdown-toggle{background-color:#d8ca47;background-image:-moz-linear-gradient(top,#c2b00c,#f9f0a0);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c2b00c),to(#f9f0a0));background-image:-webkit-linear-gradient(top,#c2b00c,#f9f0a0);background-image:-o-linear-gradient(top,#c2b00c,#f9f0a0);background-image:linear-gradient(to bottom,#c2b00c,#f9f0a0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc2b00c',endColorstr='#fff9f0a0',GradientType=0)}#navbar .navbar-inner.green{background-color:#98f064;background-image:-moz-linear-gradient(top,#c8ffa7,#50da00);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c8ffa7),to(#50da00));background-image:-webkit-linear-gradient(top,#c8ffa7,#50da00);background-image:-o-linear-gradient(top,#c8ffa7,#50da00);background-image:linear-gradient(to bottom,#c8ffa7,#50da00);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc8ffa7',endColorstr='#ff50da00',GradientType=0)}#navbar .navbar-inner.green .brand,#navbar .navbar-inner.green .nav>li>a{text-shadow:0 1px 0 #a7ff74;color:#333}#navbar .navbar-inner.green .brand .caret,#navbar .navbar-inner.green .nav>li>a .caret{border-bottom-color:#55992e;border-top-color:#55992e}#navbar .navbar-inner.green .brand:hover .caret,#navbar .navbar-inner.green .nav>li>a:hover .caret,#navbar .navbar-inner.green .brand:focus .caret,#navbar .navbar-inner.green .nav>li>a:focus .caret{border-bottom-color:#446630;border-top-color:#446630}#navbar .navbar-inner.green .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.green .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner.green .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.green .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.green .nav li.dropdown.open.active>.dropdown-toggle{background-color:#80e943;background-image:-moz-linear-gradient(top,#50da00,#c8ffa7);background-image:-webkit-gradient(linear,0 0,0 100%,from(#50da00),to(#c8ffa7));background-image:-webkit-linear-gradient(top,#50da00,#c8ffa7);background-image:-o-linear-gradient(top,#50da00,#c8ffa7);background-image:linear-gradient(to bottom,#50da00,#c8ffa7);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff50da00',endColorstr='#ffc8ffa7',GradientType=0)}#navbar .navbar-inner.blue{background-color:#6498f0;background-image:-moz-linear-gradient(top,#a7c8ff,#0050da);background-image:-webkit-gradient(linear,0 0,0 100%,from(#a7c8ff),to(#0050da));background-image:-webkit-linear-gradient(top,#a7c8ff,#0050da);background-image:-o-linear-gradient(top,#a7c8ff,#0050da);background-image:linear-gradient(to bottom,#a7c8ff,#0050da);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffa7c8ff',endColorstr='#ff0050da',GradientType=0)}#navbar .navbar-inner.blue .brand,#navbar .navbar-inner.blue .nav>li>a{text-shadow:0 1px 0 #74a7ff;color:#333}#navbar .navbar-inner.blue .brand .caret,#navbar .navbar-inner.blue .nav>li>a .caret{border-bottom-color:#2e5599;border-top-color:#2e5599}#navbar .navbar-inner.blue .brand:hover .caret,#navbar .navbar-inner.blue .nav>li>a:hover .caret,#navbar .navbar-inner.blue .brand:focus .caret,#navbar .navbar-inner.blue .nav>li>a:focus .caret{border-bottom-color:#304466;border-top-color:#304466}#navbar .navbar-inner.blue .brand span{background-image:url(../img/tentacle-20x20.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.blue .brand span{background-image:url(../img/tentacle-20x20@2x.png)}}#navbar .navbar-inner.blue .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.blue .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.blue .nav li.dropdown.open.active>.dropdown-toggle{background-color:#4380e9;background-image:-moz-linear-gradient(top,#0050da,#a7c8ff);background-image:-webkit-gradient(linear,0 0,0 100%,from(#0050da),to(#a7c8ff));background-image:-webkit-linear-gradient(top,#0050da,#a7c8ff);background-image:-o-linear-gradient(top,#0050da,#a7c8ff);background-image:linear-gradient(to bottom,#0050da,#a7c8ff);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0050da',endColorstr='#ffa7c8ff',GradientType=0)}#navbar .navbar-inner.violet{background-color:#9864f0;background-image:-moz-linear-gradient(top,#c8a7ff,#5000da);background-image:-webkit-gradient(linear,0 0,0 100%,from(#c8a7ff),to(#5000da));background-image:-webkit-linear-gradient(top,#c8a7ff,#5000da);background-image:-o-linear-gradient(top,#c8a7ff,#5000da);background-image:linear-gradient(to bottom,#c8a7ff,#5000da);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffc8a7ff',endColorstr='#ff5000da',GradientType=0)}#navbar .navbar-inner.violet .brand,#navbar .navbar-inner.violet .nav>li>a{text-shadow:0 1px 0 #5000da;color:#f2f2f2}#navbar .navbar-inner.violet .brand .caret,#navbar .navbar-inner.violet .nav>li>a .caret{border-bottom-color:#b58df9;border-top-color:#b58df9}#navbar .navbar-inner.violet .brand:hover .caret,#navbar .navbar-inner.violet .nav>li>a:hover .caret,#navbar .navbar-inner.violet .brand:focus .caret,#navbar .navbar-inner.violet .nav>li>a:focus .caret{border-bottom-color:#d3c0f5;border-top-color:#d3c0f5}#navbar .navbar-inner.violet .brand span{background-image:url(../img/tentacle-20x20-light.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.violet .brand span{background-image:url(../img/tentacle-20x20-light@2x.png)}}#navbar .navbar-inner.violet .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.violet .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.violet .nav li.dropdown.open.active>.dropdown-toggle{background-color:#8043e9;background-image:-moz-linear-gradient(top,#5000da,#c8a7ff);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5000da),to(#c8a7ff));background-image:-webkit-linear-gradient(top,#5000da,#c8a7ff);background-image:-o-linear-gradient(top,#5000da,#c8a7ff);background-image:linear-gradient(to bottom,#5000da,#c8a7ff);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5000da',endColorstr='#ffc8a7ff',GradientType=0)}#navbar .navbar-inner.black{background-color:#4f4f4f;background-image:-moz-linear-gradient(top,#787878,#121212);background-image:-webkit-gradient(linear,0 0,0 100%,from(#787878),to(#121212));background-image:-webkit-linear-gradient(top,#787878,#121212);background-image:-o-linear-gradient(top,#787878,#121212);background-image:linear-gradient(to bottom,#787878,#121212);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff787878',endColorstr='#ff121212',GradientType=0)}#navbar .navbar-inner.black .brand,#navbar .navbar-inner.black .nav>li>a{text-shadow:0 1px 0 #121212;color:#f2f2f2}#navbar .navbar-inner.black .brand .caret,#navbar .navbar-inner.black .nav>li>a .caret{border-bottom-color:#959595;border-top-color:#959595}#navbar .navbar-inner.black .brand:hover .caret,#navbar .navbar-inner.black .nav>li>a:hover .caret,#navbar .navbar-inner.black .brand:focus .caret,#navbar .navbar-inner.black .nav>li>a:focus .caret{border-bottom-color:#c4c4c4;border-top-color:#c4c4c4}#navbar .navbar-inner.black .brand span{background-image:url(../img/tentacle-20x20-light.png)}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){#navbar .navbar-inner.black .brand span{background-image:url(../img/tentacle-20x20-light@2x.png)}}#navbar .navbar-inner.black .nav li.dropdown.open>.dropdown-toggle,#navbar .navbar-inner.black .nav li.dropdown.active>.dropdown-toggle,#navbar .navbar-inner.black .nav li.dropdown.open.active>.dropdown-toggle{background-color:#3b3b3b;background-image:-moz-linear-gradient(top,#121212,#787878);background-image:-webkit-gradient(linear,0 0,0 100%,from(#121212),to(#787878));background-image:-webkit-linear-gradient(top,#121212,#787878);background-image:-o-linear-gradient(top,#121212,#787878);background-image:linear-gradient(to bottom,#121212,#787878);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff121212',endColorstr='#ff787878',GradientType=0)}#navbar .navbar-inner .brand span{background-size:20px 20px;background-position:left center;padding-left:24px;background-repeat:no-repeat}.octoprint-container .tab-content{padding:9px 15px;border-left:1px solid #DDD;border-right:1px solid #DDD;border-bottom:1px solid #DDD;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px}.octoprint-container .nav{margin-bottom:0}.octoprint-container .tab-content h1{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333;border:0;border-bottom:1px solid #e5e5e5;font-weight:normal}.octoprint-container .accordion-heading .accordion-heading-button{float:right}.octoprint-container .accordion-heading .accordion-heading-button a{display:inline-block;padding:8px 15px;font-size:14px;line-height:20px;color:#000;text-decoration:none;background:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.octoprint-container .accordion-heading a.accordion-toggle{display:inline-block}.octoprint-container .accordion-heading [class^="icon-"],.octoprint-container .accordion-heading [class*=" icon-"]{color:#000}.print-control .btn{padding-left:4px;padding-right:4px}.upload-buttons .btn{margin-right:0}table{table-layout:fixed}table .popover-title{text-overflow:ellipsis;word-break:break-all}table th,table td{overflow:hidden}table th.gcode_files_name,table td.gcode_files_name{text-overflow:ellipsis;white-space:nowrap;text-align:left}table th.gcode_files_size,table td.gcode_files_size{text-align:right;width:55px}table th.gcode_files_action,table td.gcode_files_action{text-align:center;width:70px}table th.gcode_files_action a,table td.gcode_files_action a{text-decoration:none;color:#000}table th.gcode_files_action a.disabled,table td.gcode_files_action a.disabled{color:#ccc;cursor:default}table th.timelapse_files_name,table td.timelapse_files_name{text-overflow:ellipsis;text-align:left}table th.timelapse_files_size,table td.timelapse_files_size{text-align:right;width:55px}table th.timelapse_files_action,table td.timelapse_files_action{text-align:center;width:45px}table th.timelapse_files_action a,table td.timelapse_files_action a{text-decoration:none;color:#000}table th.timelapse_files_action a.disabled,table td.timelapse_files_action a.disabled{color:#ccc;cursor:default}table th.settings_users_name,table td.settings_users_name{text-overflow:ellipsis;text-align:left}table th.settings_users_active,table td.settings_users_active,table th.settings_users_admin,table td.settings_users_admin{text-align:center;width:55px}table th.settings_users_actions,table td.settings_users_actions{text-align:center;width:60px}table th.settings_users_actions a,table td.settings_users_actions a{text-decoration:none;color:#000}table th.settings_users_actions a.disabled,table td.settings_users_actions a.disabled{color:#ccc;cursor:default}table th.settings_logs_name,table td.settings_logs_name{text-overflow:ellipsis;text-align:left}table th.settings_logs_size,table td.settings_logs_size{text-align:right;width:70px}table th.settings_logs_date,table td.settings_logs_date{text-align:left;width:130px}table th.settings_logs_action,table td.settings_logs_action{text-align:center;width:70px}table th.settings_logs_action a,table td.settings_logs_action a{text-decoration:none;color:#000}table th.settings_logs_action a.disabled,table td.settings_logs_action a.disabled{color:#ccc;cursor:default}#temperature-graph{height:350px;width:100%;background-image:url("../img/graph-background.png");background-position:center;background-repeat:no-repeat}.tab-content,.tab-pane{overflow:visible}.tempInput{width:50px}#temp_newTemp,#temp_newBedTemp,#speed_innerWall,#speed_outerWall,#speed_fill,#speed_support,#webcam_timelapse_interval{text-align:right}ul.dropdown-menu li a{cursor:pointer}#connection_ports,#connection_baudrates{width:100%}#offline_overlay{position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;display:none}#offline_overlay_background{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#000;filter:alpha(opacity=50);-moz-opacity:.5;-khtml-opacity:.5;opacity:.5}#offline_overlay_wrapper{position:absolute;top:0;bottom:0;left:0;right:0;padding-top:60px}#offline_overlay_wrapper .container{margin:auto}#webcam_container{width:100%}#webcam_container .flipH{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1)}#webcam_container .flipV{-webkit-transform:scaleY(-1);-moz-transform:scaleY(-1)}#webcam_container .flipH.flipV{-webkit-transform:scaleX(-1) scaleY(-1);-moz-transform:scaleX(-1) scaleY(-1)}#files .popover{font-size:85%}#files .popover p:last-child{margin-bottom:0}#files table{margin-bottom:0}#control{overflow:hidden}#control .jog-panel{float:left;margin-right:19px}#control h1{text-align:left}#control .jog-panel>div{text-align:center}#control .jog-panel>div.distance{text-align:left}#control .box{width:30px;height:30px;margin-right:10px;margin-bottom:10px;padding-left:8px}#control .control-box{display:block;height:30px;margin-bottom:10px}#control .btn-group{margin-bottom:10px}#control .btn-group.distance>.btn{width:43px;padding:3px 0;height:30px}#gcode .progress{width:582px}#gcode .progress .bar{-webkit-transition:width 0s linear;-moz-transition:width 0s linear;-o-transition:width 0s linear;transition:width 0s linear}#gcode #gcode_layer_slider{height:568px;float:right}#gcode #gcode_layer_slider .slider-handle{width:14px;height:14px;margin-left:-3px;margin-top:-7px}#gcode #gcode_command_slider .slider-handle{width:14px;height:14px;margin-left:-7px;margin-top:-3px}#term #terminal-output{min-height:340px}.footer ul{margin:0}.footer ul li{display:inline;margin-left:1em;font-size:85%}.footer ul li:first-child{margin-left:0}.footer ul li a{color:#555}.text-right{text-align:right}.overflow_visible{overflow:visible!important}#drop_overlay{position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;display:none}#drop_overlay.in{display:block}#drop_overlay #drop_overlay_background{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#000;filter:alpha(opacity=50);-moz-opacity:.5;-khtml-opacity:.5;opacity:.5}#drop_overlay #drop_overlay_wrapper{position:absolute;top:0;bottom:0;left:0;right:0;padding-top:60px}#drop_overlay #drop_overlay_wrapper #drop,#drop_overlay #drop_overlay_wrapper #drop_background{position:absolute;top:50%;left:50%;margin-left:-200px;margin-top:-200px}#drop_overlay #drop_overlay_wrapper #drop_locally,#drop_overlay #drop_overlay_wrapper #drop_locally_background{position:absolute;top:50%;left:50%;margin-left:-425px;margin-top:-200px}#drop_overlay #drop_overlay_wrapper #drop_sd,#drop_overlay #drop_overlay_wrapper #drop_sd_background{position:absolute;top:50%;left:50%;margin-left:25px;margin-top:-200px}#drop_overlay #drop_overlay_wrapper .dropzone{width:404px;height:404px;z-index:10001;color:#fff;font-size:30px}#drop_overlay #drop_overlay_wrapper .dropzone i{font-size:50px}#drop_overlay #drop_overlay_wrapper .dropzone .centered{display:table-cell;text-align:center;vertical-align:middle;width:400px;height:400px;line-height:40px;filter:alpha(opacity=100);-moz-opacity:1.0;-khtml-opacity:1.0;opacity:1.0}#drop_overlay #drop_overlay_wrapper .dropzone_background{width:400px;height:400px;border:2px dashed #eee;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;background-color:#000;filter:alpha(opacity=25);-moz-opacity:.25;-khtml-opacity:.25;opacity:.25}#drop_overlay #drop_overlay_wrapper .dropzone_background.hover{background-color:#000;filter:alpha(opacity=50);-moz-opacity:.5;-khtml-opacity:.5;opacity:.5}#drop_overlay #drop_overlay_wrapper .dropzone_background.fade{-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-ms-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out;opacity:1}.icon-sd-black-14{background:url("../img/icon-sd-black-14.png") 0 3px no-repeat;width:11px;height:17px}.slider .slider-selection{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#04c;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.slider .slider-selection:hover,.slider .slider-selection:focus,.slider .slider-selection:active,.slider .slider-selection.active,.slider .slider-selection.disabled,.slider .slider-selection[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.slider .slider-selection:active,.slider .slider-selection.active{background-color:#039 \9}.slider.slider-disabled .slider-selection{background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.slider .slider-track{background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.slider.slider-disabled .slider-track{background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.slider .slider-handle{display:inline-block;*display:inline;*zoom:1;padding:4px 12px;font-size:14px;line-height:20px;text-align:center;vertical-align:middle;cursor:pointer;color:#333;text-shadow:0 1px 1px rgba(255,255,255,0.75);background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#e6e6e6;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border:1px solid #ccc;*border:0;border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*margin-left:.3em;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);padding:0;margin-bottom:0;opacity:1;filter:alpha(opacity=100)}.slider .slider-handle:hover,.slider .slider-handle:focus,.slider .slider-handle:active,.slider .slider-handle.active,.slider .slider-handle.disabled,.slider .slider-handle[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.slider .slider-handle:active,.slider .slider-handle.active{background-color:#ccc \9}.slider .slider-handle:first-child{*margin-left:0}.slider .slider-handle:hover,.slider .slider-handle:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.slider .slider-handle:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.slider .slider-handle.active,.slider .slider-handle:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.slider .slider-handle.disabled,.slider .slider-handle[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.slider .slider-handle.hide{display:none}.slider .slider-handle.round{-webkit-border-radius:50%;-moz-border-radius:50%;border-radius:50%} \ No newline at end of file diff --git a/src/octoprint/static/js/app/viewmodels/log.js b/src/octoprint/static/js/app/viewmodels/log.js index 1f8af2d5..4f407303 100644 --- a/src/octoprint/static/js/app/viewmodels/log.js +++ b/src/octoprint/static/js/app/viewmodels/log.js @@ -13,7 +13,7 @@ function LogViewModel(loginStateViewModel) { if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1; return 0; }, - "creation": function(a, b) { + "modification": function(a, b) { // sorts descending if (a["date"] > b["date"]) return -1; if (a["date"] < b["date"]) return 1; diff --git a/src/octoprint/static/less/octoprint.less b/src/octoprint/static/less/octoprint.less index 8b9c7222..bbff59ce 100644 --- a/src/octoprint/static/less/octoprint.less +++ b/src/octoprint/static/less/octoprint.less @@ -279,6 +279,38 @@ table { } } } + + // log files + &.settings_logs_name { + text-overflow: ellipsis; + text-align: left; + } + + &.settings_logs_size { + text-align: right; + width: 70px; + } + + &.settings_logs_date { + text-align: left; + width: 130px; + } + + &.settings_logs_action { + text-align: center; + width: 70px; + + a { + text-decoration: none; + color: #000; + + &.disabled { + color: #ccc; + cursor: default; + } + } + } + } } diff --git a/src/octoprint/templates/settings.jinja2 b/src/octoprint/templates/settings.jinja2 index 340a12e7..6b373253 100644 --- a/src/octoprint/templates/settings.jinja2 +++ b/src/octoprint/templates/settings.jinja2 @@ -542,22 +542,24 @@ - - - + + + + - - - + + + From b976461c290dfe43bf3bf83bb46d746b0ce09417 Mon Sep 17 00:00:00 2001 From: Chris Kosmakos Date: Sun, 23 Feb 2014 19:02:00 -0800 Subject: [PATCH 15/18] minor cleanup on command history. removing unneeded var. --- src/octoprint/static/js/app/viewmodels/terminal.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/octoprint/static/js/app/viewmodels/terminal.js b/src/octoprint/static/js/app/viewmodels/terminal.js index 076ae607..68c36366 100644 --- a/src/octoprint/static/js/app/viewmodels/terminal.js +++ b/src/octoprint/static/js/app/viewmodels/terminal.js @@ -22,7 +22,6 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) { self.filterRegex = undefined; self.cmdHistory = []; - self.cmdHistoryCnt = 0; self.cmdHistoryIdx = -1; self.activeFilters = ko.observableArray([]); @@ -109,8 +108,8 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) { contentType: "application/json; charset=UTF-8", data: JSON.stringify({"command": command}) }); - self.cmdHistory[self.cmdHistoryCnt++] = command - self.cmdHistoryIdx = self.cmdHistoryCnt + self.cmdHistory.push(command); + self.cmdHistoryIdx = self.cmdHistory.length; self.command(""); } } @@ -119,11 +118,11 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) { if (event.keyCode == 13) { self.sendCommand(); } else if (event.keyCode == 38) { - if (self.cmdHistoryIdx >= 0) { + if (self.cmdHistory.length >= 0 && self.cmdHistoryIdx >= 0) { self.command(self.cmdHistory[--self.cmdHistoryIdx]) } } else if (event.keyCode == 40) { - if (self.cmdHistoryIdx < self.cmdHistoryCnt) { + if (self.cmdHistoryIdx < self.cmdHistory.length) { self.command(self.cmdHistory[++self.cmdHistoryIdx]) } } From ace102e1c18499f409380b23973387dfa570f986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 24 Feb 2014 09:57:51 +0100 Subject: [PATCH 16/18] Added API documentation, adjusted API and returned data model slightly to be consistent with other parts of the API, made log file management restricted to admins. TODO: Make downloading of logs also restricted to admins --- docs/api/index.rst | 2 +- docs/api/logs.rst | 177 ++++++++++++++++++++++++ src/octoprint/server/__init__.py | 2 +- src/octoprint/server/api/files.py | 8 +- src/octoprint/server/api/log.py | 34 +++-- src/octoprint/server/util.py | 55 +++++++- src/octoprint/templates/settings.jinja2 | 2 +- 7 files changed, 253 insertions(+), 27 deletions(-) create mode 100644 docs/api/logs.rst diff --git a/docs/api/index.rst b/docs/api/index.rst index 8d10a202..7d296a1b 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -12,4 +12,4 @@ API Documentation connection.rst printer.rst job.rst - + logs.rst diff --git a/docs/api/logs.rst b/docs/api/logs.rst new file mode 100644 index 00000000..abdb9ceb --- /dev/null +++ b/docs/api/logs.rst @@ -0,0 +1,177 @@ +.. _sec-api-logs: + +******************* +Log file management +******************* + +.. note:: + + All log file management operations require admin rights. + +.. contents:: + +.. _sec-api-logs-list: + +Retrieve a list of available log files +====================================== + +.. http:post:: /api/logs + + Retrieve information regarding all log files currently available and regarding the disk space still available + in the system on the location the log files are being stored. + + Returns a :ref:`Logfile Retrieve response `. + + **Example Request** + + .. sourcecode:: http + + GET /api/logs HTTP/1.1 + Host: example.com + X-Api-Key: abcdef... + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "files" : [ + { + "date" : 1393158814, + "name" : "octoprint.log", + "size" : 43712, + "refs": { + "resource": "http://example.com/api/logs/octoprint.log", + "download": "http://example.com/downloads/logs/octoprint.log" + } + }, + { + "date" : 1392628936, + "name" : "octoprint.log.2014-02-17", + "size" : 13205, + "refs": { + "resource": "http://example.com/api/logs/octoprint.log.2014-02-17", + "download": "http://example.com/downloads/logs/octoprint.log.2014-02-17" + } + }, + { + "date" : 1393158814, + "name" : "serial.log", + "size" : 1798419, + "refs": { + "resource": "http://example.com/api/logs/serial.log", + "download": "http://example.com/downloads/logs/serial.log" + } + } + ], + "free": 12237201408 + } + + :statuscode 200: No error + :statuscode 403: If the given API token did not have admin rights associated with it + +.. _sec-api-logs-delete: + +Delete a specific logfile +========================= + +.. http:delete:: /api/logs/(path:filename) + + Delete the selected log file with name `filename`. + + Returns a :http:statuscode:`204` after successful deletion. + + **Example Request** + + .. sourcecode:: http + + DELETE /api/logs/octoprint.log.2014-02-17 HTTP/1.1 + Host: example.com + X-Api-Key: abcdef... + + :param filename: The filename of the log file to delete + :statuscode 204: No error + :statuscode 403: If the given API token did not have admin rights associated with it + :statuscode 404: If the file was not found + +.. _sec-api-logs-datamodel: + +Datamodel +========= + +.. _sec-api-logs-datamodel-retrieveresponse: + +Logfile Retrieve Response +------------------------- + +.. list-table:: + :widths: 15 5 10 30 + :header-rows: 1 + + * - Name + - Multiplicity + - Type + - Description + * - ``files`` + - 0..* + - Array of :ref:`File information items ` + - The list of requested files. Might be an empty list if no files are available + * - ``free`` + - 1 + - String + - The amount of disk space in bytes available in the local disk space (refers to OctoPrint's ``logs`` folder). + +.. _sec-api-logs-datamodel-fileinfo: + +File information +---------------- + +.. list-table:: + :widths: 15 5 10 30 + :header-rows: 1 + + * - Name + - Multiplicity + - Type + - Description + * - ``name`` + - 1 + - String + - The name of the file + * - ``size`` + - 1 + - Number + - The size of the file in bytes. + * - ``date`` + - 1 + - Unix timestamp + - The timestamp when this file was last modified. + * - ``refs`` + - 1 + - :ref:`sec-api-logs-datamodel-ref` + - References relevant to this file + +.. _sec-api-logs-datamodel-ref: + +References +---------- + +.. list-table:: + :widths: 15 5 10 30 + :header-rows: 1 + + * - Name + - Multiplicity + - Type + - Description + * - ``resource`` + - 1 + - URL + - The resource that represents the file (e.g. for deleting) + * - ``download`` + - 1 + - URL + - The download URL for the file diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index aa6af8de..79689adc 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -28,7 +28,7 @@ admin_permission = Permission(RoleNeed("admin")) user_permission = Permission(RoleNeed("user")) -from octoprint.server.util import LargeResponseHandler, ReverseProxied, restricted_access, PrinterStateConnection +from octoprint.server.util import LargeResponseHandler, ReverseProxied, restricted_access, PrinterStateConnection, admin_validator from octoprint.printer import Printer, getConnectionOptions from octoprint.settings import settings import octoprint.gcodefiles as gcodefiles diff --git a/src/octoprint/server/api/files.py b/src/octoprint/server/api/files.py index c704faf2..bba7b260 100644 --- a/src/octoprint/server/api/files.py +++ b/src/octoprint/server/api/files.py @@ -11,7 +11,6 @@ import octoprint.util as util from octoprint.filemanager.destinations import FileDestinations from octoprint.settings import settings, valid_boolean_trues from octoprint.server import printer, gcodeManager, eventManager, restricted_access, NO_CONTENT -from octoprint.server.util import urlForDownload from octoprint.server.api import api @@ -66,7 +65,7 @@ def _getFileList(origin): file.update({ "refs": { "resource": url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=file["name"], _external=True), - "download": urlForDownload(FileDestinations.LOCAL, file["name"]) + "download": url_for("index", _external=True) + "downloads/files/" + FileDestinations.LOCAL + "/" + file["name"] } }) return files @@ -169,7 +168,7 @@ def uploadGcodeFile(target): "origin": FileDestinations.LOCAL, "refs": { "resource": url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=filename, _external=True), - "download": urlForDownload(FileDestinations.LOCAL, filename) + "download": url_for("index", _external=True) + "downloads/files/" + FileDestinations.LOCAL + "/" + filename } } }) @@ -269,6 +268,5 @@ def deleteGcodeFile(filename, target): else: gcodeManager.removeFile(filename) - # return an updated list of files - return readGcodeFiles() + return NO_CONTENT diff --git a/src/octoprint/server/api/log.py b/src/octoprint/server/api/log.py index c2699628..44159d27 100644 --- a/src/octoprint/server/api/log.py +++ b/src/octoprint/server/api/log.py @@ -4,35 +4,43 @@ __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agp import os -from flask import request, jsonify, make_response, url_for +from flask import request, jsonify, url_for, make_response from werkzeug.utils import secure_filename from octoprint.settings import settings -from octoprint.server import restricted_access +from octoprint.server import restricted_access, NO_CONTENT, admin_permission from octoprint.server.util import redirectToTornado from octoprint.server.api import api +from octoprint.util import getFreeBytes @api.route("/logs", methods=["GET"]) -def getLogData(): +@restricted_access +@admin_permission.require(403) +def getLogFiles(): files = _getLogFiles() - return jsonify(files=files) + return jsonify(files=files, free=getFreeBytes(settings().getBaseFolder("logs"))) -@api.route("/logs/", methods=["GET"]) +@api.route("/logs/", methods=["GET"]) +@restricted_access +@admin_permission.require(403) def downloadLog(filename): return redirectToTornado(request, url_for("index") + "downloads/logs/" + filename) -@api.route("/logs/", methods=["DELETE"]) +@api.route("/logs/", methods=["DELETE"]) @restricted_access +@admin_permission.require(403) def deleteLog(filename): secure = os.path.join(settings().getBaseFolder("logs"), secure_filename(filename)) - if os.path.exists(secure): - os.remove(secure) + if not os.path.exists(secure): + return make_response("File not found: %s" % filename, 404) - return getLogData() + os.remove(secure) + + return NO_CONTENT def _getLogFiles(): @@ -42,10 +50,12 @@ def _getLogFiles(): statResult = os.stat(os.path.join(basedir, osFile)) files.append({ "name": osFile, - "size": statResult.st_size, - "bytes": statResult.st_size, "date": int(statResult.st_mtime), - "url": url_for("index") + "downloads/logs/" + osFile + "size": statResult.st_size, + "refs": { + "resource": url_for(".downloadLog", filename=osFile, _external=True), + "download": url_for("index", _external=True) + "downloads/logs/" + osFile + } }) return files diff --git a/src/octoprint/server/util.py b/src/octoprint/server/util.py index 3ff9389d..2741441e 100644 --- a/src/octoprint/server/util.py +++ b/src/octoprint/server/util.py @@ -1,7 +1,11 @@ +# coding=utf-8 +__author__ = "Gina Häußge " +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' + from flask.ext.principal import identity_changed, Identity from tornado.web import StaticFileHandler, HTTPError from flask import url_for, make_response, request, current_app -from flask.ext.login import login_required, login_user +from flask.ext.login import login_required, login_user, current_user from werkzeug.utils import redirect from sockjs.tornado import SockJSConnection @@ -21,6 +25,7 @@ import octoprint.server from octoprint.users import ApiUser from octoprint.events import Events + def restricted_access(func, apiEnabled=True): """ If you decorate a view with this, it will ensure that first setup has been @@ -80,11 +85,32 @@ def api_access(func): return decorated_view +def _getUserForApiKey(apikey): + if settings().get(["api", "enabled"]) and apikey is not None: + if apikey == settings().get(["api", "key"]): + # master key was used + return ApiUser() + else: + # user key might have been used + return octoprint.server.userManager.findUser(apikey=apikey) + else: + return None + + def _getApiKey(request): - if "apikey" in request.values: + # Check Flask GET/POST arguments + if hasattr(request, "values") and "apikey" in request.values: return request.values["apikey"] + + # Check Tornado GET/POST arguments + if hasattr(request, "arguments") and "apikey" in request.arguments \ + and len(request.arguments["apikey"]) > 0 and len(request.arguments["apikey"].strip()) > 0: + return request.arguments["apikey"] + + # Check Tornado and Flask headers if "X-Api-Key" in request.headers.keys(): return request.headers.get("X-Api-Key") + return None @@ -204,11 +230,15 @@ class LargeResponseHandler(StaticFileHandler): CHUNK_SIZE = 16 * 1024 - def initialize(self, path, default_filename=None, as_attachment=False): + def initialize(self, path, default_filename=None, as_attachment=False, access_validation=None): StaticFileHandler.initialize(self, path, default_filename) self._as_attachment = as_attachment + self._access_validation = access_validation def get(self, path, include_body=True): + if self._access_validation is not None: + self._access_validation(self.request) + path = self.parse_url_path(path) abspath = os.path.abspath(os.path.join(self.root, path)) # os.path.abspath strips a trailing / @@ -273,6 +303,21 @@ class LargeResponseHandler(StaticFileHandler): self.set_header("Content-Disposition", "attachment") +#~~ admin access validator for tornado + + +# TODO doesnt work for flask right now (no app context of course), try to figure out something +def admin_validator(request): + apikey = _getApiKey(request) + if settings().get(["api", "enabled"]) and apikey is not None: + user = _getUserForApiKey(apikey) + else: + user = current_user + + if user is None or not user.is_authenticated() or not user.is_admin(): + raise HTTPError(403) + + #~~ reverse proxy compatible wsgi middleware @@ -333,7 +378,3 @@ def redirectToTornado(request, target): redirectUrl += fragment return redirect(redirectUrl) - -def urlForDownload(origin, filename): - return url_for("index", _external=True) + "downloads/files/" + origin + "/" + filename - diff --git a/src/octoprint/templates/settings.jinja2 b/src/octoprint/templates/settings.jinja2 index 6b373253..802f1ac8 100644 --- a/src/octoprint/templates/settings.jinja2 +++ b/src/octoprint/templates/settings.jinja2 @@ -560,7 +560,7 @@ From f844bd99c7242d8a7817e43c55a40e1d3ed83fbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 24 Feb 2014 14:13:55 +0100 Subject: [PATCH 17/18] Properly authenticate log file downloads on Tornado as well Only allow for users with role admin. --- src/octoprint/server/__init__.py | 19 +++++++++++++++++-- src/octoprint/server/util.py | 12 ++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 79689adc..51bf09f9 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -2,6 +2,8 @@ __author__ = "Gina Häußge " __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' +import flask +import tornado.wsgi from sockjs.tornado import SockJSRouter from flask import Flask, render_template, send_from_directory, make_response from flask.ext.login import LoginManager @@ -27,7 +29,7 @@ principals = Principal(app) admin_permission = Permission(RoleNeed("admin")) user_permission = Permission(RoleNeed("user")) - +# only import the octoprint stuff down here, as it might depend on things defined above to be initialized already from octoprint.server.util import LargeResponseHandler, ReverseProxied, restricted_access, PrinterStateConnection, admin_validator from octoprint.printer import Printer, getConnectionOptions from octoprint.settings import settings @@ -174,10 +176,23 @@ class Server(): self._router = SockJSRouter(self._createSocketConnection, "/sockjs") + def admin_access_validation(request): + """ + Creates a custom wsgi and Flask request context in order to be able to process user information + stored in the current session. + + :param request: The Tornado request for which to create the environment and context + """ + wsgi_environ = tornado.wsgi.WSGIContainer.environ(request) + with app.request_context(wsgi_environ): + app.session_interface.open_session(app, flask.request) + loginManager.reload_user() + admin_validator(flask.request) + self._tornado_app = Application(self._router.urls + [ (r"/downloads/timelapse/([^/]*\.mpg)", LargeResponseHandler, {"path": settings().getBaseFolder("timelapse"), "as_attachment": True}), (r"/downloads/files/local/([^/]*\.(gco|gcode))", LargeResponseHandler, {"path": settings().getBaseFolder("uploads"), "as_attachment": True}), - (r"/downloads/logs/([^/]*)", LargeResponseHandler, {"path": settings().getBaseFolder("logs"), "as_attachment": True}), + (r"/downloads/logs/([^/]*)", LargeResponseHandler, {"path": settings().getBaseFolder("logs"), "as_attachment": True, "access_validation": admin_access_validation}), (r".*", FallbackHandler, {"fallback": WSGIContainer(app.wsgi_app)}) ]) self._server = HTTPServer(self._tornado_app) diff --git a/src/octoprint/server/util.py b/src/octoprint/server/util.py index 2741441e..71149686 100644 --- a/src/octoprint/server/util.py +++ b/src/octoprint/server/util.py @@ -303,11 +303,19 @@ class LargeResponseHandler(StaticFileHandler): self.set_header("Content-Disposition", "attachment") -#~~ admin access validator for tornado +#~~ admin access validator for use with tornado -# TODO doesnt work for flask right now (no app context of course), try to figure out something def admin_validator(request): + """ + Validates that the given request is made by an admin user, identified either by API key or existing Flask + session. + + Must be executed in an existing Flask request context! + + :param request: The Flask request object + """ + apikey = _getApiKey(request) if settings().get(["api", "enabled"]) and apikey is not None: user = _getUserForApiKey(apikey) From 45c9e75e4e9fa9a1e687bbf354b36d5796e0ab95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 24 Feb 2014 21:57:02 +0100 Subject: [PATCH 18/18] Slight refactoring of a PR Corrected indentation, removed jumpy cursor, fixed off-by-one errors on history limits, limited history to a maximum of 300 entries ("300 items of command history should really be enough for anybody!") --- .../static/js/app/viewmodels/terminal.js | 68 +++++++++++++------ src/octoprint/templates/index.jinja2 | 2 +- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/src/octoprint/static/js/app/viewmodels/terminal.js b/src/octoprint/static/js/app/viewmodels/terminal.js index 68c36366..ad8240f3 100644 --- a/src/octoprint/static/js/app/viewmodels/terminal.js +++ b/src/octoprint/static/js/app/viewmodels/terminal.js @@ -33,25 +33,25 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) { self.fromCurrentData = function(data) { self._processStateData(data.state); self._processCurrentLogData(data.logs); - } + }; self.fromHistoryData = function(data) { self._processStateData(data.state); self._processHistoryLogData(data.logHistory); - } + }; self._processCurrentLogData = function(data) { if (!self.log) - self.log = [] - self.log = self.log.concat(data) - self.log = self.log.slice(-300) + self.log = []; + self.log = self.log.concat(data); + self.log = self.log.slice(-300); self.updateOutput(); - } + }; self._processHistoryLogData = function(data) { self.log = data; self.updateOutput(); - } + }; self._processStateData = function(data) { self.isErrorOrClosed(data.flags.closedOrError); @@ -61,7 +61,7 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) { self.isError(data.flags.error); self.isReady(data.flags.ready); self.isLoading(data.flags.loading); - } + }; self.updateFilterRegex = function() { var filterRegexStr = self.activeFilters().join("|").trim(); @@ -71,7 +71,7 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) { self.filterRegex = new RegExp(filterRegexStr); } console.log("Terminal filter regex: " + filterRegexStr); - } + }; self.updateOutput = function() { if (!self.log) @@ -89,10 +89,13 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) { if (self.autoscrollEnabled()) { container.scrollTop(container[0].scrollHeight - container.height()) } - } + }; self.sendCommand = function() { var command = self.command(); + if (!command) { + return; + } var re = /^([gmt][0-9]+)(\s.*)?/; var commandMatch = command.match(re); @@ -108,24 +111,45 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) { contentType: "application/json; charset=UTF-8", data: JSON.stringify({"command": command}) }); - self.cmdHistory.push(command); + + self.cmdHistory.push(command); + self.cmdHistory.slice(-300); // just to set a sane limit to how many manually entered commands will be saved... self.cmdHistoryIdx = self.cmdHistory.length; self.command(""); } - } + }; - self.handleKeys = function(event) { + self.handleKeyDown = function(event) { + var keyCode = event.keyCode; + + if (keyCode == 38 || keyCode == 40) { + if (keyCode == 38 && self.cmdHistory.length > 0 && self.cmdHistoryIdx > 0) { + self.cmdHistoryIdx--; + } else if (keyCode == 40 && self.cmdHistoryIdx < self.cmdHistory.length - 1) { + self.cmdHistoryIdx++; + } + + if (self.cmdHistoryIdx >= 0 && self.cmdHistoryIdx < self.cmdHistory.length) { + self.command(self.cmdHistory[self.cmdHistoryIdx]); + } + + // prevent the cursor from being moved to the beginning of the input field (this is actually the reason + // why we do the arrow key handling in the keydown event handler, keyup would be too late already to + // prevent this from happening, causing a jumpy cursor) + return false; + } + + // do not prevent default action + return true; + }; + + self.handleKeyUp = function(event) { if (event.keyCode == 13) { self.sendCommand(); - } else if (event.keyCode == 38) { - if (self.cmdHistory.length >= 0 && self.cmdHistoryIdx >= 0) { - self.command(self.cmdHistory[--self.cmdHistoryIdx]) - } - } else if (event.keyCode == 40) { - if (self.cmdHistoryIdx < self.cmdHistory.length) { - self.command(self.cmdHistory[++self.cmdHistoryIdx]) - } } - } + + // do not prevent default action + return true; + }; } diff --git a/src/octoprint/templates/index.jinja2 b/src/octoprint/templates/index.jinja2 index c73b92a0..01fdd4de 100644 --- a/src/octoprint/templates/index.jinja2 +++ b/src/octoprint/templates/index.jinja2 @@ -517,7 +517,7 @@
    NameSizeActionNameSizeDateAction
    +  | 
    -  |  +  |