summaryrefslogtreecommitdiff
path: root/mobmonitor
diff options
context:
space:
mode:
authorMatthew Sartori <msartori@chromium.org>2015-07-20 15:02:47 -0700
committerChromeOS Commit Bot <chromeos-commit-bot@chromium.org>2015-08-20 01:38:02 +0000
commitdb5cb304ecf29e9d9855375c2c2630c6a487496d (patch)
tree66a575ded9493e5339607b2aa28bfcbf66037b1f /mobmonitor
parent170286d7fd116a98d17098830a6c7afece3148cf (diff)
downloadchromite-db5cb304ecf29e9d9855375c2c2630c6a487496d.tar.gz
mobmonitor: Mob* Monitor Web UI.
This CL adds a simple and clean web interface for viewing service health statuses and requesting repair operators. CQ-DEPEND=CL:290308 BUG=chromium:490822 TEST=Deployed to moblab and tested the web interface manually. Change-Id: I67f57ab578e2732fabc8a414dfaf27ed550a1d7b Reviewed-on: https://chromium-review.googlesource.com/290238 Tested-by: Matthew Sartori <msartori@chromium.org> Reviewed-by: Nick Matthijssen <nickam@google.com> Commit-Queue: Matthew Sartori <msartori@chromium.org>
Diffstat (limited to 'mobmonitor')
-rwxr-xr-xmobmonitor/scripts/mobmonitor.py43
-rw-r--r--mobmonitor/scripts/mobmonitor_unittest.py18
-rw-r--r--mobmonitor/static/css/style.css126
-rw-r--r--mobmonitor/static/js/actionrepairdialog.js131
-rw-r--r--mobmonitor/static/js/healthdisplay.js144
-rw-r--r--mobmonitor/static/js/main.js44
-rw-r--r--mobmonitor/static/js/rpc.js40
-rw-r--r--mobmonitor/static/js/template.js31
-rw-r--r--mobmonitor/static/js/util.js20
-rw-r--r--mobmonitor/static/templates/actionrepairdialog.html16
-rw-r--r--mobmonitor/static/templates/healthstatuscontainer.html36
-rw-r--r--mobmonitor/static/templates/index.html26
12 files changed, 664 insertions, 11 deletions
diff --git a/mobmonitor/scripts/mobmonitor.py b/mobmonitor/scripts/mobmonitor.py
index c6a637d39..ed709b919 100755
--- a/mobmonitor/scripts/mobmonitor.py
+++ b/mobmonitor/scripts/mobmonitor.py
@@ -9,6 +9,7 @@ from __future__ import print_function
import cherrypy
import json
+import os
import sys
from chromite.lib import remote_access
@@ -16,16 +17,23 @@ from chromite.lib import commandline
from chromite.mobmonitor.checkfile import manager
+STATICDIR = '/etc/mobmonitor/static'
+
+
class MobMonitorRoot(object):
"""The central object supporting the Mob* Monitor web interface."""
- def __init__(self, checkfile_manager):
+ def __init__(self, checkfile_manager, staticdir=STATICDIR):
+ if not os.path.exists(staticdir):
+ raise IOError('Static directory does not exist: %s' % staticdir)
+
+ self.staticdir = staticdir
self.checkfile_manager = checkfile_manager
@cherrypy.expose
def index(self):
"""Presents a welcome message."""
- return 'Welcome to the Mob* Monitor!'
+ return open(os.path.join(self.staticdir, 'templates', 'index.html'))
@cherrypy.expose
def GetServiceList(self):
@@ -87,6 +95,8 @@ def ParseArguments(argv):
help='The Mob* Monitor checkfile directory.')
parser.add_argument('-p', '--port', type=int, default=9991,
help='The Mob* Monitor port.')
+ parser.add_argument('-s', '--staticdir', default=STATICDIR,
+ help='Mob* monitor web ui static content directory')
return parser.parse_args(argv)
@@ -95,18 +105,37 @@ def main(argv):
options = ParseArguments(argv)
options.Freeze()
- # Start the Mob* Monitor web interface.
- cherrypy.config.update({'server.socket_port':
- remote_access.NormalizePort(options.port)})
+ # Configure global cherrypy parameters.
+ cherrypy.config.update(
+ {'server.socket_host': '0.0.0.0',
+ 'server.socket_port': remote_access.NormalizePort(options.port)
+ })
+
+ mobmon_appconfig = {
+ '/':
+ {'tools.staticdir.root': options.staticdir
+ },
+ '/static':
+ {'tools.staticdir.on': True,
+ 'tools.staticdir.dir': ''
+ },
+ '/static/css':
+ {'tools.staticdir.dir': 'css'
+ },
+ '/static/js':
+ {'tools.staticdir.dir': 'js'
+ }
+ }
# Setup the mobmonitor
checkfile_manager = manager.CheckFileManager(checkdir=options.checkdir)
- mobmonitor = MobMonitorRoot(checkfile_manager)
+ mobmonitor = MobMonitorRoot(checkfile_manager, staticdir=options.staticdir)
# Start the checkfile collection and execution background task.
checkfile_manager.StartCollectionExecution()
- cherrypy.quickstart(mobmonitor)
+ # Start the Mob* Monitor.
+ cherrypy.quickstart(mobmonitor, config=mobmon_appconfig)
if __name__ == '__main__':
diff --git a/mobmonitor/scripts/mobmonitor_unittest.py b/mobmonitor/scripts/mobmonitor_unittest.py
index 8e4e90875..babce4bd9 100644
--- a/mobmonitor/scripts/mobmonitor_unittest.py
+++ b/mobmonitor/scripts/mobmonitor_unittest.py
@@ -7,8 +7,10 @@
from __future__ import print_function
import json
+import os
from chromite.lib import cros_test_lib
+from chromite.lib import osutils
from chromite.mobmonitor.checkfile import manager
from chromite.mobmonitor.scripts import mobmonitor
@@ -39,19 +41,27 @@ class MockCheckFileManager(object):
return self.service_statuses[0]
-class MobMonitorRootTest(cros_test_lib.MockTestCase):
+class MobMonitorRootTest(cros_test_lib.MockTempDirTestCase):
"""Unittests for the MobMonitorRoot."""
+ STATICDIR = 'static'
+
+ def setUp(self):
+ """Setup directories expected by the Mob* Monitor."""
+ self.mobmondir = self.tempdir
+ self.staticdir = os.path.join(self.mobmondir, self.STATICDIR)
+ osutils.SafeMakedirs(self.staticdir)
+
def testGetServiceList(self):
"""Test the GetServiceList RPC."""
cfm = MockCheckFileManager()
- root = mobmonitor.MobMonitorRoot(cfm)
+ root = mobmonitor.MobMonitorRoot(cfm, staticdir=self.staticdir)
self.assertEqual(cfm.GetServiceList(), json.loads(root.GetServiceList()))
def testGetStatus(self):
"""Test the GetStatus RPC."""
cfm = MockCheckFileManager()
- root = mobmonitor.MobMonitorRoot(cfm)
+ root = mobmonitor.MobMonitorRoot(cfm, staticdir=self.staticdir)
# Test the result for a single service.
status = cfm.service_statuses[0]
@@ -73,7 +83,7 @@ class MobMonitorRootTest(cros_test_lib.MockTestCase):
def testRepairService(self):
"""Test the RepairService RPC."""
cfm = MockCheckFileManager()
- root = mobmonitor.MobMonitorRoot(cfm)
+ root = mobmonitor.MobMonitorRoot(cfm, staticdir=self.staticdir)
status = cfm.service_statuses[0]
expect = {'service': status.service, 'health': status.health,
diff --git a/mobmonitor/static/css/style.css b/mobmonitor/static/css/style.css
new file mode 100644
index 000000000..957ee230a
--- /dev/null
+++ b/mobmonitor/static/css/style.css
@@ -0,0 +1,126 @@
+/* General Style Settings */
+body {
+ background-color: #788999;
+ margin: 0;
+ padding: 0;
+}
+
+table {
+ border-collapse: collapse;
+}
+
+td {
+ border: 1px solid #999;
+ padding: 5px;
+ text-align: left;
+}
+
+.bold {
+ font-weight: bold;
+}
+
+.site-header {
+ background-color: #20262c;
+ padding-top: 50px;
+ padding-right: 0px;
+ padding-left: 0px;
+ padding-bottom: 50px;
+ margin-bottom: 50px;
+}
+
+.site-header-title {
+ max-width: 1000px;
+ width: 90%;
+ margin: 0px auto;
+ margin-top: 0px;
+ margin-right: auto;
+ margin-bottom: 0px;
+ margin-left: auto;
+ text-align: center;
+ font-size: 32pt;
+ font-weight: bold;
+ color: #ACD6FF;
+}
+
+/* Health Display Style Settings */
+.health-display {
+ max-width: 1000px;
+ width: 90%;
+ margin: 0px auto;
+ margin-top: 0px;
+ margin-right: auto;
+ margin-bottom: 0px;
+ margin-left: auto;
+}
+
+.health-container {
+ border: 1px solid black;
+ background-color: #FFF;
+}
+
+.health-container-header {
+ background-color: #FFFAAA;
+
+ float: left;
+ width: 100%;
+}
+
+.header-service-name {
+ float: left;
+ width: 80%;
+}
+
+.header-service-status {
+ float: left;
+}
+
+.header-service-status-button {
+}
+
+.health-container-content {
+ background-color: #FFF;
+ padding: 10px;
+}
+
+.healthcheck-table {
+ margin: auto;
+ border: 1px solid black;
+}
+
+.healthcheck {
+ background-color: grey;
+}
+
+.color-healthy {
+ color: green;
+}
+
+.color-unhealthy {
+ color: red;
+}
+
+.color-quasi-healthy {
+ color: orange;
+}
+
+/* Action Repair Style Settings */
+.actionlist-dropdown {
+ width:95%;
+}
+
+.input-text {
+ margin-bottom: 10px;
+ width: 95%;
+}
+
+.actiondisplay {
+ width: 95%;
+ height: 150px;
+ resize: none;
+}
+
+fieldset {
+ padding: 0;
+ border: 0;
+ margin-top: 10px;
+}
diff --git a/mobmonitor/static/js/actionrepairdialog.js b/mobmonitor/static/js/actionrepairdialog.js
new file mode 100644
index 000000000..8888df446
--- /dev/null
+++ b/mobmonitor/static/js/actionrepairdialog.js
@@ -0,0 +1,131 @@
+// Copyright 2015 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+'use strict';
+
+
+function ActionRepairDialog(service, actions) {
+ var actionRepairDialog = this;
+
+ var templateData = {
+ actions: actions
+ };
+
+ this.service = service;
+ this.actionsToSubmit = [];
+
+ this.dialogElement_ = $(
+ renderTemplate('actionrepairdialog', templateData)).dialog({
+ autoOpen: false,
+ height: 525,
+ width: 550,
+ modal: true,
+
+ close: function(event, ui) {
+ $(this).dialog('destroy').remove();
+ },
+
+ buttons: {
+ 'Clear Actions': function() {
+ actionRepairDialog.clear();
+ },
+ 'Stage Action': function() {
+ actionRepairDialog.stage();
+ },
+ 'Submit': function() {
+ actionRepairDialog.submit();
+ }
+ }
+ });
+
+ var d = this.dialogElement_;
+ this.dialogActionListDropdown = $(d).find('.actionlist-dropdown')[0];
+ this.dialogArgs = $(d).find('#args')[0];
+ this.dialogKwargs = $(d).find('#kwargs')[0];
+ this.dialogStagedActions = $(d).find('#stagedActions')[0];
+}
+
+ActionRepairDialog.prototype.open = function() {
+ this.dialogElement_.dialog('open');
+};
+
+ActionRepairDialog.prototype.close = function() {
+ this.dialogElement_.dialog('close');
+};
+
+ActionRepairDialog.prototype.clear = function() {
+ this.dialogActionListDropdown.value = '';
+ this.dialogArgs.value = '';
+ this.dialogKwargs.value = '';
+ this.dialogStagedActions.value = '';
+ this.actionsToSubmit = [];
+};
+
+ActionRepairDialog.prototype.stage = function() {
+ var action = this.dialogActionListDropdown.value;
+ var args = this.dialogArgs.value;
+ var kwargs = this.dialogKwargs.value;
+
+ // Validate the action input.
+ if (!action) {
+ alert('An action must be selected to stage.');
+ return;
+ }
+
+ if (args && !/^([^,]+,)*[^,]+$/g.test(args)) {
+ alert('Arguments are not well-formed.\n' +
+ 'Expected form: a1,a2,...,aN');
+ return;
+ }
+
+ if (kwargs && !/^([^,=]+=[^,=]+,)*[^,]+=[^,=]+$/g.test(kwargs)) {
+ alert('Keyword argumetns are not well-formed.\n' +
+ 'Expected form: kw1=foo,...,kwN=bar');
+ return;
+ }
+
+ // Store the action and add it to the staged action display.
+ var storedArgs = args ? args.split(',') : [];
+ var storedKwargs = {};
+ kwargs.split(',').forEach(function(elem, index, array) {
+ var kv = elem.split('=');
+ storedKwargs[kv[0]] = kv[1];
+ });
+
+ var stagedAction = {
+ action: action,
+ args: storedArgs,
+ kwargs: storedKwargs
+ };
+
+ this.actionsToSubmit.push(stagedAction);
+ this.dialogStagedActions.value += JSON.stringify(stagedAction);
+};
+
+ActionRepairDialog.prototype.submit = function() {
+ // Caller must define the function 'submitHandler' on the created dialog.
+ // The submitHandler will be passed the following arguments:
+ // service: A string.
+ // action: A string.
+ // args: An array.
+ // kwargs: An object.
+
+ if (!this.submitHandler) {
+ alert('Caller must define submitHandler for ActionRepairDialog.');
+ return;
+ }
+
+ if (isEmpty(this.actionsToSubmit)) {
+ alert('Actions must be staged prior to submission.');
+ return;
+ }
+
+ for (var i = 0; i < this.actionsToSubmit.length; i++) {
+ var stagedAction = this.actionsToSubmit[i];
+ this.submitHandler(this.service, stagedAction.action, stagedAction.args,
+ stagedAction.kwargs);
+ }
+
+ this.close();
+};
diff --git a/mobmonitor/static/js/healthdisplay.js b/mobmonitor/static/js/healthdisplay.js
new file mode 100644
index 000000000..50af418b0
--- /dev/null
+++ b/mobmonitor/static/js/healthdisplay.js
@@ -0,0 +1,144 @@
+// Copyright 2015 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+'use strict';
+
+var RECORD_TTL_MS = 15000;
+
+
+$.widget('mobmonitor.healthDisplay', {
+ options: {},
+
+ _create: function() {
+ this.element.addClass('health-display');
+
+ // Service status information. The variable serviceHealthStatusInfo
+ // is a mapping of the following form:
+ //
+ // {serviceName: {lastUpdatedTimestampInMs: lastUpdatedTimestampInMs,
+ // serviceStatus: serviceStatus}}
+ //
+ // Where serviceStatus objects are of the following form:
+ //
+ // {serviceName: serviceNameString,
+ // health: boolean,
+ // healthchecks: [
+ // {name: healthCheckName, health: boolean,
+ // description: descriptionString,
+ // actions: [actionNameString]}
+ // ]
+ // }
+ this.serviceHealthStatusInfo = {};
+ },
+
+ _destroy: function() {
+ this.element.removeClass('health-display');
+ this.element.empty();
+ },
+
+ _setOption: function(key, value) {
+ this._super(key, value);
+ },
+
+ // Private widget methods.
+ // TODO (msartori): Implement crbug.com/520746.
+ _updateHealthDisplayServices: function(services) {
+ var self = this;
+
+ function _removeService(service) {
+ // Remove the UI elements.
+ var id = '#' + SERVICE_CONTAINER_PREFIX + service;
+ $(id).empty();
+ $(id).remove();
+
+ // Remove raw service status info that we are holding.
+ delete self.serviceHealthStatusInfo[service];
+ }
+
+ function _addService(serviceStatus) {
+ // This function is used as a callback to the rpcGetStatus.
+ // rpcGetStatus returns a list of service health statuses.
+ // In this widget, we add services one at a time, so take
+ // the first element.
+ serviceStatus = serviceStatus[0];
+
+ // Create the new content for the healthDisplay widget.
+ var templateData = jQuery.extend({}, serviceStatus);
+ templateData.serviceId = SERVICE_CONTAINER_PREFIX + serviceStatus.service;
+
+ var healthContainer = renderTemplate('healthstatuscontainer',
+ templateData);
+
+ // Insert the new container into the display widget.
+ $(healthContainer).appendTo(self.element);
+
+ // Maintain alphabetical order in our display.
+ self.element.children().sort(function(a, b) {
+ return $(a).attr('id') < $(b).attr('id') ? -1 : 1;
+ }).appendTo(self.element);
+
+ // Save information to do with this service.
+ var curtime = $.now();
+ var service = serviceStatus.service;
+
+ self.serviceHealthStatusInfo[service] = {
+ lastUpdatedTimestampInMs: curtime,
+ serviceStatus: serviceStatus
+ };
+ }
+
+ // Remove services that are no longer monitored or are stale.
+ var now = $.now();
+
+ Object.keys(this.serviceHealthStatusInfo).forEach(
+ function(elem, index, array) {
+ if ($.inArray(elem, services) < 0 ||
+ now > self.serviceHealthStatusInfo[elem].lastUpdatedTimestampInMs +
+ RECORD_TTL_MS) {
+ _removeService(elem);
+ }
+ });
+
+ // Get sublist of services to update.
+ var updateList =
+ $(services).not(Object.keys(this.serviceHealthStatusInfo)).get();
+
+ // Update the services.
+ updateList.forEach(function(elem, index, array) {
+ rpcGetStatus(elem, _addService);
+ });
+ },
+
+ // Public widget methods.
+ refreshHealthDisplay: function() {
+ var self = this;
+ rpcGetServiceList(function(services) {
+ self._updateHealthDisplayServices(services);
+ });
+ },
+
+ needsRepair: function(service) {
+ var serviceStatus = this.serviceHealthStatusInfo[service].serviceStatus;
+ return serviceStatus.health == 'false' ||
+ serviceStatus.healthchecks.length > 0;
+ },
+
+ markStale: function(service) {
+ this.serviceHealthStatusInfo[service].lastUpdatedTimestampInMs = 0;
+ },
+
+ getServiceActions: function(service) {
+ var actionSet = {};
+ var healthchecks =
+ this.serviceHealthStatusInfo[service].serviceStatus.healthchecks;
+
+ for (var i = 0; i < healthchecks.length; i++) {
+ for (var j = 0; j < healthchecks[i].actions.length; j++) {
+ actionSet[healthchecks[i].actions[j]] = true;
+ }
+ }
+
+ return Object.keys(actionSet);
+ }
+});
diff --git a/mobmonitor/static/js/main.js b/mobmonitor/static/js/main.js
new file mode 100644
index 000000000..ff80d4614
--- /dev/null
+++ b/mobmonitor/static/js/main.js
@@ -0,0 +1,44 @@
+// Copyright 2015 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+'use strict';
+
+
+$(document).ready(function() {
+ // Setup the health status widget.
+ $('#healthStatusDisplay').healthDisplay();
+ $('#healthStatusDisplay').healthDisplay('refreshHealthDisplay');
+
+ setInterval(function() {
+ $('#healthStatusDisplay').healthDisplay('refreshHealthDisplay');
+ }, 3000);
+
+ // Setup the action repair dialog popup.
+ // TODO(msartori): Implement crbug.com/520749.
+ $(document).on('click', '.header-service-status-button', function() {
+ // Get the service that this button corresponds to.
+ var service = $(this).closest('.health-container').attr('id');
+ if (service.indexOf(SERVICE_CONTAINER_PREFIX) === 0) {
+ service = service.replace(SERVICE_CONTAINER_PREFIX, '');
+ }
+
+ // Do not launch dialog if this service does not need repair.
+ if (!$('#healthStatusDisplay').healthDisplay('needsRepair', service)) {
+ return;
+ }
+
+ // Get the actions for this service.
+ var actions = $('#healthStatusDisplay').healthDisplay(
+ 'getServiceActions', service);
+
+ // Create and launch the action repair dialog.
+ var dialog = new ActionRepairDialog(service, actions);
+ dialog.submitHandler = function(service, action, args, kwargs) {
+ rpcRepairService(service, action, args, kwargs, function(response) {
+ $('#healthStatusDisplay').healthDisplay('markStale', response.service);
+ });
+ };
+ dialog.open();
+ });
+});
diff --git a/mobmonitor/static/js/rpc.js b/mobmonitor/static/js/rpc.js
new file mode 100644
index 000000000..b85367afb
--- /dev/null
+++ b/mobmonitor/static/js/rpc.js
@@ -0,0 +1,40 @@
+// Copyright 2015 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+'use strict';
+
+
+function rpcGetServiceList(callback) {
+ $.getJSON('/GetServiceList', function(response) {
+ callback(response);
+ });
+}
+
+function rpcGetStatus(service, callback) {
+ $.getJSON('/GetStatus', {service: service}, function(response) {
+ callback(response);
+ });
+}
+
+function rpcRepairService(service, action, args, kwargs, callback) {
+
+ if (isEmpty(service))
+ throw new InvalidRpcArgumentError(
+ 'Must specify service in RepairService RPC');
+
+ if (isEmpty(action))
+ throw new InvalidRpcArgumentError(
+ 'Must specify action in RepairService RPC');
+
+ var data = {
+ service: service,
+ action: action,
+ args: JSON.stringify(args),
+ kwargs: JSON.stringify(kwargs)
+ };
+
+ $.post('/RepairService', data, function(response) {
+ callback(response);
+ }, 'json');
+}
diff --git a/mobmonitor/static/js/template.js b/mobmonitor/static/js/template.js
new file mode 100644
index 000000000..7950f0ce8
--- /dev/null
+++ b/mobmonitor/static/js/template.js
@@ -0,0 +1,31 @@
+// Copyright 2015 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+'use strict';
+
+
+function renderTemplate(templateName, templateData) {
+ if (!renderTemplate.cache) {
+ renderTemplate.cache = {};
+ }
+
+ if (!renderTemplate.cache[templateName]) {
+ var dir = '/static/templates';
+ var url = dir + '/' + templateName + '.html';
+
+ var templateString;
+ $.ajax({
+ url: url,
+ method: 'GET',
+ async: false,
+ success: function(data) {
+ templateString = data;
+ }
+ });
+
+ renderTemplate.cache[templateName] = Handlebars.compile(templateString);
+ }
+
+ return renderTemplate.cache[templateName](templateData);
+}
diff --git a/mobmonitor/static/js/util.js b/mobmonitor/static/js/util.js
new file mode 100644
index 000000000..1609c9b6a
--- /dev/null
+++ b/mobmonitor/static/js/util.js
@@ -0,0 +1,20 @@
+// Copyright 2015 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+'use strict';
+
+var SERVICE_CONTAINER_PREFIX = 'serviceHealthContainer';
+
+
+function InvalidRpcArgumentError(message) {
+ this.message = message;
+}
+InvalidRpcArgumentError.prototype = new Error;
+
+function isEmpty(x) {
+ if (typeof(x) === 'undefined' || x === null)
+ return true;
+
+ return $.isEmptyObject(x);
+}
diff --git a/mobmonitor/static/templates/actionrepairdialog.html b/mobmonitor/static/templates/actionrepairdialog.html
new file mode 100644
index 000000000..1b65a0c27
--- /dev/null
+++ b/mobmonitor/static/templates/actionrepairdialog.html
@@ -0,0 +1,16 @@
+<div id="actionRepairDialog" title="Setup Repair Actions">
+ <label for="actionlist">Actions</label>
+ <input list="actionlist" class="actionlist-dropdown">
+ <datalist id="actionlist">
+ {{#each actions}}
+ <option value={{this}}>
+ {{/each}}
+ </datalist>
+ <label for="args">Arguments</label>
+ <input type="text" name="args" id="args" value="" class="input-text">
+ <label for="kwargs">Keyword Arguments</label>
+ <input type="text" name="kwargs" id="kwargs" value="" class="input-text">
+ <label for="stagedActions">Actions to Execute</label>
+ <textarea readonly name="stagedActions" id="stagedActions" class="actiondisplay">
+ </textarea>
+</div>
diff --git a/mobmonitor/static/templates/healthstatuscontainer.html b/mobmonitor/static/templates/healthstatuscontainer.html
new file mode 100644
index 000000000..014160bed
--- /dev/null
+++ b/mobmonitor/static/templates/healthstatuscontainer.html
@@ -0,0 +1,36 @@
+<div id={{serviceId}} class="health-container">
+ <div class="health-container-header">
+ <div class="header-service-name">{{service}}</div>
+ <div class="header-service-status">
+ {{#if healthchecks.length}}
+ {{#if health}}
+ <button class="header-service-status-button color-quasi-healthy">Healthy</button>
+ {{else}}
+ <button class="header-service-status-button color-unhealthy">Unhealthy</button>
+ {{/if}}
+ {{else}}
+ <button class="header-service-status-button color-healthy">Healthy</button>
+ {{/if}}
+ </div>
+ </div>
+ {{#if healthchecks.length}}
+ <div class="health-container-content">
+ <table class="healthcheck-table">
+ <tr>
+ <td class="bold">Health Check Name</td>
+ <td class="bold">Health</td>
+ <td class="bold">Description</td>
+ <td class="bold">Actions</td>
+ </tr>
+ {{#each healthchecks}}
+ <tr>
+ <td>{{name}}</td>
+ <td>{{health}}</td>
+ <td>{{description}}</td>
+ <td>{{actions}}</td>
+ </tr>
+ {{/each}}
+ </table>
+ </div>
+ {{/if}}
+</div>
diff --git a/mobmonitor/static/templates/index.html b/mobmonitor/static/templates/index.html
new file mode 100644
index 000000000..761f63aa5
--- /dev/null
+++ b/mobmonitor/static/templates/index.html
@@ -0,0 +1,26 @@
+<html>
+ <head>
+ <title>Mob* Monitor</title>
+ <link href="/static/css/style.css" rel="stylesheet">
+ <link href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css" rel="stylesheet">
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
+ <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/3.0.3/handlebars.js"></script>
+ <script src="/static/js/util.js"></script>
+ <script src="/static/js/template.js"></script>
+ <script src="/static/js/rpc.js"></script>
+ <script src="/static/js/actionrepairdialog.js"></script>
+ <script src="/static/js/healthdisplay.js"></script>
+ <script src="/static/js/main.js"></script>
+ </head>
+
+ <body>
+ <header class="site-header">
+ <div class="site-header-title">Mob* Monitor</div>
+ </header>
+
+ <div id="healthStatusDisplay">
+ </div>
+
+ </body>
+</html>