diff --git a/.gitignore b/.gitignore
index 1e7dbf9a..fc90aca3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ docs/build
 .idea/
 config
 platypush/backend/http/static/css/*/.sass-cache/
+.vscode
diff --git a/platypush/__main__.py b/platypush/__main__.py
index 30fd0e97..19417792 100644
--- a/platypush/__main__.py
+++ b/platypush/__main__.py
@@ -3,4 +3,3 @@ from platypush import main
 main()
 
 # vim:sw=4:ts=4:et:
-
diff --git a/platypush/backend/http/app/routes/dashboard.py b/platypush/backend/http/app/routes/dashboard.py
index e129c79c..7340ddf8 100644
--- a/platypush/backend/http/app/routes/dashboard.py
+++ b/platypush/backend/http/app/routes/dashboard.py
@@ -1,6 +1,6 @@
 from flask import Blueprint, request, render_template
 
-from platypush.backend.http.app import template_folder
+from platypush.backend.http.app import template_folder, static_folder
 from platypush.backend.http.app.utils import authenticate, authentication_ok, \
     get_websocket_port
 
@@ -14,17 +14,21 @@ __routes__ = [
     dashboard,
 ]
 
+
 @dashboard.route('/dashboard', methods=['GET'])
 def dashboard():
     """ Route for the fullscreen dashboard """
-    if not authentication_ok(request): return authenticate()
+    if not authentication_ok(request):
+        return authenticate()
 
     http_conf = Config.get('backend.http')
     dashboard_conf = http_conf.get('dashboard', {})
 
     return render_template('dashboard.html', config=dashboard_conf,
                            utils=HttpUtils, token=Config.get('token'),
-                           websocket_port=get_websocket_port())
+                           static_folder=static_folder, template_folder=template_folder,
+                           websocket_port=get_websocket_port(),
+                           has_ssl=http_conf.get('ssl_cert') is not None)
 
 
 # vim:sw=4:ts=4:et:
diff --git a/platypush/backend/http/app/routes/map.py b/platypush/backend/http/app/routes/map.py
deleted file mode 100644
index d6e10ca7..00000000
--- a/platypush/backend/http/app/routes/map.py
+++ /dev/null
@@ -1,95 +0,0 @@
-import datetime
-import dateutil.parser
-
-from flask import abort, request, render_template, Blueprint
-
-from platypush.backend.http.app import template_folder
-from platypush.backend.http.app.utils import authenticate, authentication_ok
-from platypush.config import Config
-
-
-map_ = Blueprint('map', __name__, template_folder=template_folder)
-
-# Declare routes list
-__routes__ = [
-    map_,
-]
-
-def parse_time(time_string):
-    if not time_string:
-        return None
-
-    now = datetime.datetime.now()
-
-    if time_string == 'now':
-        return now.isoformat()
-    if time_string == 'yesterday':
-        return (now - datetime.timedelta(days=1)).isoformat()
-
-    try:
-        return dateutil.parser.parse(time_string).isoformat()
-    except ValueError:
-        pass
-
-    m = re.match('([-+]?)([0-9]+)([dhms])', time_string)
-    if not m:
-        raise RuntimeError('Invalid time interval string representation: "{}"'.
-                           format(time_string))
-
-    time_delta = (-1 if m.group(1) == '-' else 1) * int(m.group(2))
-    time_unit = m.group(3)
-
-    if time_unit == 'd':
-        params = { 'days': time_delta }
-    elif time_unit == 'h':
-        params = { 'hours': time_delta }
-    elif time_unit == 'm':
-        params = { 'minutes': time_delta }
-    elif time_unit == 's':
-        params = { 'seconds': time_delta }
-
-    return (now + datetime.timedelta(**params)).isoformat()
-
-
-@map_.route('/map', methods=['GET'])
-def map():
-    """
-    Query parameters:
-        start -- Map timeline start timestamp
-        end   -- Map timeline end timestamp
-        zoom  -- Between 1-20. Set it if you want to override the
-            Google's API auto-zoom. You may have to set it if you are
-            trying to embed the map into an iframe
-
-    Supported values for `start` and `end`:
-        - now
-        - yesterday
-        - -30s (it means '30 seconds ago')
-        - -10m (it means '10 minutes ago')
-        - -24h (it means '24 hours ago')
-        - -7d  (it means '7 days ago')
-        - 2018-06-04T17:39:22.742Z (ISO strings)
-
-    Default: start=yesterday, end=now
-    """
-
-    if not authentication_ok(request): return authenticate()
-    map_conf = (Config.get('backend.http') or {}).get('maps', {})
-    if not map_conf:
-        abort(500, 'The maps plugin is not configured in backend.http')
-
-    api_key = map_conf.get('api_key')
-    if not api_key:
-        abort(500, 'Google Maps api_key not set in the maps configuration')
-
-    start = parse_time(request.args.get('start', default='yesterday'))
-    end = parse_time(request.args.get('end', default='now'))
-    zoom = request.args.get('zoom', default=None)
-
-    return render_template('map.html', config=map_conf, utils=HttpUtils,
-                           start=start, end=end, zoom=zoom,
-                           token=Config.get('token'), api_key=api_key,
-                           websocket_port=get_websocket_port())
-
-
-# vim:sw=4:ts=4:et:
diff --git a/platypush/backend/http/static/css/application.css b/platypush/backend/http/static/css/application.css
deleted file mode 100644
index 149dcd91..00000000
--- a/platypush/backend/http/static/css/application.css
+++ /dev/null
@@ -1,227 +0,0 @@
-body {
-    width: 100%;
-    overflow-x: hidden;
-}
-
-header {
-    width: 100%;
-    background: #f4f5f6;
-    padding: 10px 25px;
-    margin: 0px 10px 35px -10px;
-    border-bottom: 1px solid #e1e4e8;
-}
-
-.logo {
-    font-size: 25px;
-    margin-top: 10px;
-}
-
-    .logo > .logo-1 {
-        font-weight: bold;
-    }
-
-.modal {
-    display: none;
-    position: absolute;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    overflow: hidden;
-    z-index: 999;
-    background-color: rgba(10,10,10,0.85);
-    font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
-}
-
-    .modal-container {
-        margin: 5% auto auto auto;
-        width: 70%;
-        background: white;
-        border-radius: 10px;
-    }
-
-        .modal-header {
-            border-bottom: 1px solid #ccc;
-            margin: 0.5rem auto;
-            padding: 0.5rem;
-            text-align: center;
-            background-color: #f0f0f0;
-            border-radius: 10px 10px 0 0;
-            text-transform: uppercase;
-            font-weight: 400px;
-            letter-spacing: .1rem;
-            line-height: 38px;
-        }
-
-        .modal-body {
-            padding: 2.5rem 2rem 1.5rem 2rem;
-        }
-
-        .form-footer {
-            text-align: right;
-            margin-top: 2rem;
-            border-top: 1px solid #ddd;
-        }
-
-            .form-footer * > input[type=button],
-            .form-footer * > button {
-                margin-top: 2rem;
-                text-transform: uppercase;
-                font-size: 1.3rem;
-            }
-
-#date-time {
-    text-align: right;
-    padding-right: 30px;
-}
-
-    #date-time > .date {
-        color: #666;
-    }
-
-    #date-time > .time {
-        font-weight: bold;
-        font-size: 30px;
-    }
-
-main {
-    width: 95%;
-    margin: auto;
-}
-
-    ul.tab-nav {
-        padding-left: 0 !important;  /* Override skeleton-tabs default */
-    }
-
-.tab-content {
-    border: 1px solid #bbb;
-    border-top: 0;
-    padding: 20px;
-    margin-top: -25px;
-    border-radius: 0 0 7.5px 7.5px;
-}
-
-button[disabled] {
-    color: #bbb;
-    border: 1px solid;
-}
-
-    .button[disabled] {
-        background: rgba(240,240,240,1)
-    }
-
-.slider {
-    -webkit-appearance: none;
-    appearance: none;
-    width: 100%;
-    height: 15px;
-    border-radius: 5px;
-    background: #e4e4e4;
-    outline: none;
-    opacity: 0.7;
-    -webkit-transition: .2s;
-    transition: opacity .2s;
-}
-
-    .slider:hover {
-        opacity: 1;
-    }
-
-    .slider::-webkit-slider-thumb {
-        -webkit-appearance: none;
-        appearance: none;
-        width: 25px;
-        height: 25px;
-        border-radius: 50%;
-        background: #4CAF50;
-        cursor: pointer;
-    }
-
-        .slider[disabled]::-webkit-slider-thumb {
-            display: none;
-        }
-
-    .slider::-moz-range-thumb {
-        width: 25px;
-        height: 25px;
-        background: #4CAF50;
-        cursor: pointer;
-    }
-
-.btn-primary {
-    background-color: #d8ffe0 !important;
-    border: 1px solid #98efb0 !important;
-}
-
-.right-side {
-    text-align: right !important;
-}
-
-#notification-container {
-    position: fixed;
-    bottom: 0;
-    right: 0;
-    width: 25em;
-}
-
-    #notification-container > .notification {
-        background: rgba(180,245,188,0.85);
-        border: 1px solid #bbb;
-        border-radius: 5px;
-        margin-bottom: 1rem;
-        margin-right: 1rem;
-        cursor: pointer;
-        display: none;
-    }
-
-    #notification-container > .notification:hover {
-        background: rgba(160,235,168,0.9);
-    }
-
-    #notification-container * > .notification-title {
-        padding: 4px 10px;
-        font-weight: bold;
-        text-transform: capitalize;
-        line-height: 30px;
-        letter-spacing: .1rem;
-    }
-
-    #notification-container * > .notification-body {
-        height: 6em;
-        overflow: hidden;
-        padding-bottom: 1rem;
-        letter-spacing: .05rem;
-    }
-
-    #notification-container * > .notification-text {
-        margin-left: 0 !important;
-    }
-
-    #notification-container * > .notification-image {
-        height: 100%;
-        text-align: center;
-        padding-top: .6em;
-        padding-bottom: .6em;
-    }
-
-        #notification-container * > .notification-image-item {
-            width: 100%;
-            height: 100%;
-            margin: auto;
-        }
-
-        #notification-container * > .fa.notification-image-item {
-            margin-top: .8em;
-            margin-left: .2em;
-            font-size: 25px;
-        }
-
-        #notification-container * > .notification-image img {
-            width: 80%;
-            height: 80%;
-        }
-
-#hidden-plugins-container > .plugin {
-    display: none;
-}
-
diff --git a/platypush/backend/http/static/css/assistant.google.css b/platypush/backend/http/static/css/assistant.google.css
deleted file mode 100644
index e69de29b..00000000
diff --git a/platypush/backend/http/static/css/dashboard.css b/platypush/backend/http/static/css/dashboard.css
deleted file mode 100644
index 9b31fb18..00000000
--- a/platypush/backend/http/static/css/dashboard.css
+++ /dev/null
@@ -1,20 +0,0 @@
-html {
-    min-height: 100%;
-}
-
-body {
-    background-image: url('/img/dashboard-background.jpg');
-    background-size: 100% 100%;
-    background-repeat: no-repeat;
-    font-family: Lato;
-}
-
-.widget {
-    background: white;
-    margin-top: 2em;
-    border-radius: 5px;
-    height: 22.5em;
-    overflow: hidden;
-    box-shadow: 0 3px 3px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08);
-}
-
diff --git a/platypush/backend/http/static/css/gpio.css b/platypush/backend/http/static/css/gpio.css
deleted file mode 100644
index 952678fa..00000000
--- a/platypush/backend/http/static/css/gpio.css
+++ /dev/null
@@ -1,32 +0,0 @@
-#gpio-container {
-    background-color: #f8f8f8;
-    padding: 12px;
-    border: 1px solid #ddd;
-    border-radius: 10px;
-    font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
-    font-weight: 400;
-    line-height: 38px;
-    letter-spacing: .1rem;
-}
-
-.gpio-pin {
-    padding: 1.5em 1em;
-    border-radius: 10px;
-}
-
-    .gpio-pin:nth-of-type(odd) {
-        background-color: #ececec;
-    }
-
-    .gpio-pin:hover {
-        background-color: #daf8e2 !important;
-    }
-
-.pin-value {
-    text-align: right;
-}
-
-.pin-ctrl {
-    width: 8em;
-}
-
diff --git a/platypush/backend/http/static/css/gpio.sensor.mcp3008.css b/platypush/backend/http/static/css/gpio.sensor.mcp3008.css
deleted file mode 100644
index 51edfd61..00000000
--- a/platypush/backend/http/static/css/gpio.sensor.mcp3008.css
+++ /dev/null
@@ -1,25 +0,0 @@
-#sensors-container {
-    width: 80%;
-    max-width: 60rem;
-    margin: 3em auto;
-    background: rgba(235,235,235,0.8);
-    padding: 2em;
-    border: 1px solid rgba(220,220,220,1.0);
-    border-radius: 10px;
-}
-
-.sensor-data {
-    text-transform: capitalize;
-}
-
-    .sensor-data:not(:last-child) {
-        margin-bottom: .5rem;
-        padding-bottom: .5rem;
-        border-bottom: 1px solid rgba(220,220,220,1.0);
-    }
-
-.sensor-value {
-    text-align: right;
-    font-weight: bold;
-}
-
diff --git a/platypush/backend/http/static/css/gpio.zeroborg.css b/platypush/backend/http/static/css/gpio.zeroborg.css
deleted file mode 100644
index cdafb117..00000000
--- a/platypush/backend/http/static/css/gpio.zeroborg.css
+++ /dev/null
@@ -1,24 +0,0 @@
-#zb-container {
-    outline: none !important;
-}
-
-.zb-controls-container {
-    margin: auto;
-    width: 25%;
-}
-
-.zb-ctrl-btn {
-    white-space: normal;
-    height: 6rem;
-}
-
-    .zb-ctrl-btn.selected {
-        color: #78ff00 !important;
-    }
-
-.ctrl-bottom-row {
-    margin-top: 20px;
-    padding-top: 20px;
-    border-top: 1px solid #ddd;
-}
-
diff --git a/platypush/backend/http/static/css/jquery-ui.css b/platypush/backend/http/static/css/jquery-ui.css
deleted file mode 100644
index 294452f1..00000000
--- a/platypush/backend/http/static/css/jquery-ui.css
+++ /dev/null
@@ -1,1311 +0,0 @@
-/*! jQuery UI - v1.12.1 - 2016-09-14
-* http://jqueryui.com
-* Includes: core.css, accordion.css, autocomplete.css, menu.css, button.css, controlgroup.css, checkboxradio.css, datepicker.css, dialog.css, draggable.css, resizable.css, progressbar.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css
-* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
-* Copyright jQuery Foundation and other contributors; Licensed MIT */
-
-/* Layout helpers
-----------------------------------*/
-.ui-helper-hidden {
-	display: none;
-}
-.ui-helper-hidden-accessible {
-	border: 0;
-	clip: rect(0 0 0 0);
-	height: 1px;
-	margin: -1px;
-	overflow: hidden;
-	padding: 0;
-	position: absolute;
-	width: 1px;
-}
-.ui-helper-reset {
-	margin: 0;
-	padding: 0;
-	border: 0;
-	outline: 0;
-	line-height: 1.3;
-	text-decoration: none;
-	font-size: 100%;
-	list-style: none;
-}
-.ui-helper-clearfix:before,
-.ui-helper-clearfix:after {
-	content: "";
-	display: table;
-	border-collapse: collapse;
-}
-.ui-helper-clearfix:after {
-	clear: both;
-}
-.ui-helper-zfix {
-	width: 100%;
-	height: 100%;
-	top: 0;
-	left: 0;
-	position: absolute;
-	opacity: 0;
-	filter:Alpha(Opacity=0); /* support: IE8 */
-}
-
-.ui-front {
-	z-index: 100;
-}
-
-
-/* Interaction Cues
-----------------------------------*/
-.ui-state-disabled {
-	cursor: default !important;
-	pointer-events: none;
-}
-
-
-/* Icons
-----------------------------------*/
-.ui-icon {
-	display: inline-block;
-	vertical-align: middle;
-	margin-top: -.25em;
-	position: relative;
-	text-indent: -99999px;
-	overflow: hidden;
-	background-repeat: no-repeat;
-}
-
-.ui-widget-icon-block {
-	left: 50%;
-	margin-left: -8px;
-	display: block;
-}
-
-/* Misc visuals
-----------------------------------*/
-
-/* Overlays */
-.ui-widget-overlay {
-	position: fixed;
-	top: 0;
-	left: 0;
-	width: 100%;
-	height: 100%;
-}
-.ui-accordion .ui-accordion-header {
-	display: block;
-	cursor: pointer;
-	position: relative;
-	margin: 2px 0 0 0;
-	padding: .5em .5em .5em .7em;
-	font-size: 100%;
-}
-.ui-accordion .ui-accordion-content {
-	padding: 1em 2.2em;
-	border-top: 0;
-	overflow: auto;
-}
-.ui-autocomplete {
-	position: absolute;
-	top: 0;
-	left: 0;
-	cursor: default;
-}
-.ui-menu {
-	list-style: none;
-	padding: 0;
-	margin: 0;
-	display: block;
-	outline: 0;
-}
-.ui-menu .ui-menu {
-	position: absolute;
-}
-.ui-menu .ui-menu-item {
-	margin: 0;
-	cursor: pointer;
-	/* support: IE10, see #8844 */
-	list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
-}
-.ui-menu .ui-menu-item-wrapper {
-	position: relative;
-	padding: 3px 1em 3px .4em;
-}
-.ui-menu .ui-menu-divider {
-	margin: 5px 0;
-	height: 0;
-	font-size: 0;
-	line-height: 0;
-	border-width: 1px 0 0 0;
-}
-.ui-menu .ui-state-focus,
-.ui-menu .ui-state-active {
-	margin: -1px;
-}
-
-/* icon support */
-.ui-menu-icons {
-	position: relative;
-}
-.ui-menu-icons .ui-menu-item-wrapper {
-	padding-left: 2em;
-}
-
-/* left-aligned */
-.ui-menu .ui-icon {
-	position: absolute;
-	top: 0;
-	bottom: 0;
-	left: .2em;
-	margin: auto 0;
-}
-
-/* right-aligned */
-.ui-menu .ui-menu-icon {
-	left: auto;
-	right: 0;
-}
-.ui-button {
-	padding: .4em 1em;
-	display: inline-block;
-	position: relative;
-	line-height: normal;
-	margin-right: .1em;
-	cursor: pointer;
-	vertical-align: middle;
-	text-align: center;
-	-webkit-user-select: none;
-	-moz-user-select: none;
-	-ms-user-select: none;
-	user-select: none;
-
-	/* Support: IE <= 11 */
-	overflow: visible;
-}
-
-.ui-button,
-.ui-button:link,
-.ui-button:visited,
-.ui-button:hover,
-.ui-button:active {
-	text-decoration: none;
-}
-
-/* to make room for the icon, a width needs to be set here */
-.ui-button-icon-only {
-	width: 2em;
-	box-sizing: border-box;
-	text-indent: -9999px;
-	white-space: nowrap;
-}
-
-/* no icon support for input elements */
-input.ui-button.ui-button-icon-only {
-	text-indent: 0;
-}
-
-/* button icon element(s) */
-.ui-button-icon-only .ui-icon {
-	position: absolute;
-	top: 50%;
-	left: 50%;
-	margin-top: -8px;
-	margin-left: -8px;
-}
-
-.ui-button.ui-icon-notext .ui-icon {
-	padding: 0;
-	width: 2.1em;
-	height: 2.1em;
-	text-indent: -9999px;
-	white-space: nowrap;
-
-}
-
-input.ui-button.ui-icon-notext .ui-icon {
-	width: auto;
-	height: auto;
-	text-indent: 0;
-	white-space: normal;
-	padding: .4em 1em;
-}
-
-/* workarounds */
-/* Support: Firefox 5 - 40 */
-input.ui-button::-moz-focus-inner,
-button.ui-button::-moz-focus-inner {
-	border: 0;
-	padding: 0;
-}
-.ui-controlgroup {
-	vertical-align: middle;
-	display: inline-block;
-}
-.ui-controlgroup > .ui-controlgroup-item {
-	float: left;
-	margin-left: 0;
-	margin-right: 0;
-}
-.ui-controlgroup > .ui-controlgroup-item:focus,
-.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus {
-	z-index: 9999;
-}
-.ui-controlgroup-vertical > .ui-controlgroup-item {
-	display: block;
-	float: none;
-	width: 100%;
-	margin-top: 0;
-	margin-bottom: 0;
-	text-align: left;
-}
-.ui-controlgroup-vertical .ui-controlgroup-item {
-	box-sizing: border-box;
-}
-.ui-controlgroup .ui-controlgroup-label {
-	padding: .4em 1em;
-}
-.ui-controlgroup .ui-controlgroup-label span {
-	font-size: 80%;
-}
-.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item {
-	border-left: none;
-}
-.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item {
-	border-top: none;
-}
-.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content {
-	border-right: none;
-}
-.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content {
-	border-bottom: none;
-}
-
-/* Spinner specific style fixes */
-.ui-controlgroup-vertical .ui-spinner-input {
-
-	/* Support: IE8 only, Android < 4.4 only */
-	width: 75%;
-	width: calc( 100% - 2.4em );
-}
-.ui-controlgroup-vertical .ui-spinner .ui-spinner-up {
-	border-top-style: solid;
-}
-
-.ui-checkboxradio-label .ui-icon-background {
-	box-shadow: inset 1px 1px 1px #ccc;
-	border-radius: .12em;
-	border: none;
-}
-.ui-checkboxradio-radio-label .ui-icon-background {
-	width: 16px;
-	height: 16px;
-	border-radius: 1em;
-	overflow: visible;
-	border: none;
-}
-.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,
-.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon {
-	background-image: none;
-	width: 8px;
-	height: 8px;
-	border-width: 4px;
-	border-style: solid;
-}
-.ui-checkboxradio-disabled {
-	pointer-events: none;
-}
-.ui-datepicker {
-	width: 17em;
-	padding: .2em .2em 0;
-	display: none;
-}
-.ui-datepicker .ui-datepicker-header {
-	position: relative;
-	padding: .2em 0;
-}
-.ui-datepicker .ui-datepicker-prev,
-.ui-datepicker .ui-datepicker-next {
-	position: absolute;
-	top: 2px;
-	width: 1.8em;
-	height: 1.8em;
-}
-.ui-datepicker .ui-datepicker-prev-hover,
-.ui-datepicker .ui-datepicker-next-hover {
-	top: 1px;
-}
-.ui-datepicker .ui-datepicker-prev {
-	left: 2px;
-}
-.ui-datepicker .ui-datepicker-next {
-	right: 2px;
-}
-.ui-datepicker .ui-datepicker-prev-hover {
-	left: 1px;
-}
-.ui-datepicker .ui-datepicker-next-hover {
-	right: 1px;
-}
-.ui-datepicker .ui-datepicker-prev span,
-.ui-datepicker .ui-datepicker-next span {
-	display: block;
-	position: absolute;
-	left: 50%;
-	margin-left: -8px;
-	top: 50%;
-	margin-top: -8px;
-}
-.ui-datepicker .ui-datepicker-title {
-	margin: 0 2.3em;
-	line-height: 1.8em;
-	text-align: center;
-}
-.ui-datepicker .ui-datepicker-title select {
-	font-size: 1em;
-	margin: 1px 0;
-}
-.ui-datepicker select.ui-datepicker-month,
-.ui-datepicker select.ui-datepicker-year {
-	width: 45%;
-}
-.ui-datepicker table {
-	width: 100%;
-	font-size: .9em;
-	border-collapse: collapse;
-	margin: 0 0 .4em;
-}
-.ui-datepicker th {
-	padding: .7em .3em;
-	text-align: center;
-	font-weight: bold;
-	border: 0;
-}
-.ui-datepicker td {
-	border: 0;
-	padding: 1px;
-}
-.ui-datepicker td span,
-.ui-datepicker td a {
-	display: block;
-	padding: .2em;
-	text-align: right;
-	text-decoration: none;
-}
-.ui-datepicker .ui-datepicker-buttonpane {
-	background-image: none;
-	margin: .7em 0 0 0;
-	padding: 0 .2em;
-	border-left: 0;
-	border-right: 0;
-	border-bottom: 0;
-}
-.ui-datepicker .ui-datepicker-buttonpane button {
-	float: right;
-	margin: .5em .2em .4em;
-	cursor: pointer;
-	padding: .2em .6em .3em .6em;
-	width: auto;
-	overflow: visible;
-}
-.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current {
-	float: left;
-}
-
-/* with multiple calendars */
-.ui-datepicker.ui-datepicker-multi {
-	width: auto;
-}
-.ui-datepicker-multi .ui-datepicker-group {
-	float: left;
-}
-.ui-datepicker-multi .ui-datepicker-group table {
-	width: 95%;
-	margin: 0 auto .4em;
-}
-.ui-datepicker-multi-2 .ui-datepicker-group {
-	width: 50%;
-}
-.ui-datepicker-multi-3 .ui-datepicker-group {
-	width: 33.3%;
-}
-.ui-datepicker-multi-4 .ui-datepicker-group {
-	width: 25%;
-}
-.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,
-.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header {
-	border-left-width: 0;
-}
-.ui-datepicker-multi .ui-datepicker-buttonpane {
-	clear: left;
-}
-.ui-datepicker-row-break {
-	clear: both;
-	width: 100%;
-	font-size: 0;
-}
-
-/* RTL support */
-.ui-datepicker-rtl {
-	direction: rtl;
-}
-.ui-datepicker-rtl .ui-datepicker-prev {
-	right: 2px;
-	left: auto;
-}
-.ui-datepicker-rtl .ui-datepicker-next {
-	left: 2px;
-	right: auto;
-}
-.ui-datepicker-rtl .ui-datepicker-prev:hover {
-	right: 1px;
-	left: auto;
-}
-.ui-datepicker-rtl .ui-datepicker-next:hover {
-	left: 1px;
-	right: auto;
-}
-.ui-datepicker-rtl .ui-datepicker-buttonpane {
-	clear: right;
-}
-.ui-datepicker-rtl .ui-datepicker-buttonpane button {
-	float: left;
-}
-.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,
-.ui-datepicker-rtl .ui-datepicker-group {
-	float: right;
-}
-.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,
-.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header {
-	border-right-width: 0;
-	border-left-width: 1px;
-}
-
-/* Icons */
-.ui-datepicker .ui-icon {
-	display: block;
-	text-indent: -99999px;
-	overflow: hidden;
-	background-repeat: no-repeat;
-	left: .5em;
-	top: .3em;
-}
-.ui-dialog {
-	position: absolute;
-	top: 0;
-	left: 0;
-	padding: .2em;
-	outline: 0;
-}
-.ui-dialog .ui-dialog-titlebar {
-	padding: .4em 1em;
-	position: relative;
-}
-.ui-dialog .ui-dialog-title {
-	float: left;
-	margin: .1em 0;
-	white-space: nowrap;
-	width: 90%;
-	overflow: hidden;
-	text-overflow: ellipsis;
-}
-.ui-dialog .ui-dialog-titlebar-close {
-	position: absolute;
-	right: .3em;
-	top: 50%;
-	width: 20px;
-	margin: -10px 0 0 0;
-	padding: 1px;
-	height: 20px;
-}
-.ui-dialog .ui-dialog-content {
-	position: relative;
-	border: 0;
-	padding: .5em 1em;
-	background: none;
-	overflow: auto;
-}
-.ui-dialog .ui-dialog-buttonpane {
-	text-align: left;
-	border-width: 1px 0 0 0;
-	background-image: none;
-	margin-top: .5em;
-	padding: .3em 1em .5em .4em;
-}
-.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {
-	float: right;
-}
-.ui-dialog .ui-dialog-buttonpane button {
-	margin: .5em .4em .5em 0;
-	cursor: pointer;
-}
-.ui-dialog .ui-resizable-n {
-	height: 2px;
-	top: 0;
-}
-.ui-dialog .ui-resizable-e {
-	width: 2px;
-	right: 0;
-}
-.ui-dialog .ui-resizable-s {
-	height: 2px;
-	bottom: 0;
-}
-.ui-dialog .ui-resizable-w {
-	width: 2px;
-	left: 0;
-}
-.ui-dialog .ui-resizable-se,
-.ui-dialog .ui-resizable-sw,
-.ui-dialog .ui-resizable-ne,
-.ui-dialog .ui-resizable-nw {
-	width: 7px;
-	height: 7px;
-}
-.ui-dialog .ui-resizable-se {
-	right: 0;
-	bottom: 0;
-}
-.ui-dialog .ui-resizable-sw {
-	left: 0;
-	bottom: 0;
-}
-.ui-dialog .ui-resizable-ne {
-	right: 0;
-	top: 0;
-}
-.ui-dialog .ui-resizable-nw {
-	left: 0;
-	top: 0;
-}
-.ui-draggable .ui-dialog-titlebar {
-	cursor: move;
-}
-.ui-draggable-handle {
-	-ms-touch-action: none;
-	touch-action: none;
-}
-.ui-resizable {
-	position: relative;
-}
-.ui-resizable-handle {
-	position: absolute;
-	font-size: 0.1px;
-	display: block;
-	-ms-touch-action: none;
-	touch-action: none;
-}
-.ui-resizable-disabled .ui-resizable-handle,
-.ui-resizable-autohide .ui-resizable-handle {
-	display: none;
-}
-.ui-resizable-n {
-	cursor: n-resize;
-	height: 7px;
-	width: 100%;
-	top: -5px;
-	left: 0;
-}
-.ui-resizable-s {
-	cursor: s-resize;
-	height: 7px;
-	width: 100%;
-	bottom: -5px;
-	left: 0;
-}
-.ui-resizable-e {
-	cursor: e-resize;
-	width: 7px;
-	right: -5px;
-	top: 0;
-	height: 100%;
-}
-.ui-resizable-w {
-	cursor: w-resize;
-	width: 7px;
-	left: -5px;
-	top: 0;
-	height: 100%;
-}
-.ui-resizable-se {
-	cursor: se-resize;
-	width: 12px;
-	height: 12px;
-	right: 1px;
-	bottom: 1px;
-}
-.ui-resizable-sw {
-	cursor: sw-resize;
-	width: 9px;
-	height: 9px;
-	left: -5px;
-	bottom: -5px;
-}
-.ui-resizable-nw {
-	cursor: nw-resize;
-	width: 9px;
-	height: 9px;
-	left: -5px;
-	top: -5px;
-}
-.ui-resizable-ne {
-	cursor: ne-resize;
-	width: 9px;
-	height: 9px;
-	right: -5px;
-	top: -5px;
-}
-.ui-progressbar {
-	height: 2em;
-	text-align: left;
-	overflow: hidden;
-}
-.ui-progressbar .ui-progressbar-value {
-	margin: -1px;
-	height: 100%;
-}
-.ui-progressbar .ui-progressbar-overlay {
-	background: url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");
-	height: 100%;
-	filter: alpha(opacity=25); /* support: IE8 */
-	opacity: 0.25;
-}
-.ui-progressbar-indeterminate .ui-progressbar-value {
-	background-image: none;
-}
-.ui-selectable {
-	-ms-touch-action: none;
-	touch-action: none;
-}
-.ui-selectable-helper {
-	position: absolute;
-	z-index: 100;
-	border: 1px dotted black;
-}
-.ui-selectmenu-menu {
-	padding: 0;
-	margin: 0;
-	position: absolute;
-	top: 0;
-	left: 0;
-	display: none;
-}
-.ui-selectmenu-menu .ui-menu {
-	overflow: auto;
-	overflow-x: hidden;
-	padding-bottom: 1px;
-}
-.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup {
-	font-size: 1em;
-	font-weight: bold;
-	line-height: 1.5;
-	padding: 2px 0.4em;
-	margin: 0.5em 0 0 0;
-	height: auto;
-	border: 0;
-}
-.ui-selectmenu-open {
-	display: block;
-}
-.ui-selectmenu-text {
-	display: block;
-	margin-right: 20px;
-	overflow: hidden;
-	text-overflow: ellipsis;
-}
-.ui-selectmenu-button.ui-button {
-	text-align: left;
-	white-space: nowrap;
-	width: 14em;
-}
-.ui-selectmenu-icon.ui-icon {
-	float: right;
-	margin-top: 0;
-}
-.ui-slider {
-	position: relative;
-	text-align: left;
-}
-.ui-slider .ui-slider-handle {
-	position: absolute;
-	z-index: 2;
-	width: 1.2em;
-	height: 1.2em;
-	cursor: default;
-	-ms-touch-action: none;
-	touch-action: none;
-}
-.ui-slider .ui-slider-range {
-	position: absolute;
-	z-index: 1;
-	font-size: .7em;
-	display: block;
-	border: 0;
-	background-position: 0 0;
-}
-
-/* support: IE8 - See #6727 */
-.ui-slider.ui-state-disabled .ui-slider-handle,
-.ui-slider.ui-state-disabled .ui-slider-range {
-	filter: inherit;
-}
-
-.ui-slider-horizontal {
-	height: .8em;
-}
-.ui-slider-horizontal .ui-slider-handle {
-	top: -.3em;
-	margin-left: -.6em;
-}
-.ui-slider-horizontal .ui-slider-range {
-	top: 0;
-	height: 100%;
-}
-.ui-slider-horizontal .ui-slider-range-min {
-	left: 0;
-}
-.ui-slider-horizontal .ui-slider-range-max {
-	right: 0;
-}
-
-.ui-slider-vertical {
-	width: .8em;
-	height: 100px;
-}
-.ui-slider-vertical .ui-slider-handle {
-	left: -.3em;
-	margin-left: 0;
-	margin-bottom: -.6em;
-}
-.ui-slider-vertical .ui-slider-range {
-	left: 0;
-	width: 100%;
-}
-.ui-slider-vertical .ui-slider-range-min {
-	bottom: 0;
-}
-.ui-slider-vertical .ui-slider-range-max {
-	top: 0;
-}
-.ui-sortable-handle {
-	-ms-touch-action: none;
-	touch-action: none;
-}
-.ui-spinner {
-	position: relative;
-	display: inline-block;
-	overflow: hidden;
-	padding: 0;
-	vertical-align: middle;
-}
-.ui-spinner-input {
-	border: none;
-	background: none;
-	color: inherit;
-	padding: .222em 0;
-	margin: .2em 0;
-	vertical-align: middle;
-	margin-left: .4em;
-	margin-right: 2em;
-}
-.ui-spinner-button {
-	width: 1.6em;
-	height: 50%;
-	font-size: .5em;
-	padding: 0;
-	margin: 0;
-	text-align: center;
-	position: absolute;
-	cursor: default;
-	display: block;
-	overflow: hidden;
-	right: 0;
-}
-/* more specificity required here to override default borders */
-.ui-spinner a.ui-spinner-button {
-	border-top-style: none;
-	border-bottom-style: none;
-	border-right-style: none;
-}
-.ui-spinner-up {
-	top: 0;
-}
-.ui-spinner-down {
-	bottom: 0;
-}
-.ui-tabs {
-	position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
-	padding: .2em;
-}
-.ui-tabs .ui-tabs-nav {
-	margin: 0;
-	padding: .2em .2em 0;
-}
-.ui-tabs .ui-tabs-nav li {
-	list-style: none;
-	float: left;
-	position: relative;
-	top: 0;
-	margin: 1px .2em 0 0;
-	border-bottom-width: 0;
-	padding: 0;
-	white-space: nowrap;
-}
-.ui-tabs .ui-tabs-nav .ui-tabs-anchor {
-	float: left;
-	padding: .5em 1em;
-	text-decoration: none;
-}
-.ui-tabs .ui-tabs-nav li.ui-tabs-active {
-	margin-bottom: -1px;
-	padding-bottom: 1px;
-}
-.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,
-.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,
-.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor {
-	cursor: text;
-}
-.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor {
-	cursor: pointer;
-}
-.ui-tabs .ui-tabs-panel {
-	display: block;
-	border-width: 0;
-	padding: 1em 1.4em;
-	background: none;
-}
-.ui-tooltip {
-	padding: 8px;
-	position: absolute;
-	z-index: 9999;
-	max-width: 300px;
-}
-body .ui-tooltip {
-	border-width: 2px;
-}
-/* Component containers
-----------------------------------*/
-.ui-widget {
-	font-family: Verdana,Arial,sans-serif;
-	font-size: 1.1em;
-}
-.ui-widget .ui-widget {
-	font-size: 1em;
-}
-.ui-widget input,
-.ui-widget select,
-.ui-widget textarea,
-.ui-widget button {
-	font-family: Verdana,Arial,sans-serif;
-	font-size: 1em;
-}
-.ui-widget.ui-widget-content {
-	border: 1px solid #d3d3d3;
-}
-.ui-widget-content {
-	border: 1px solid #aaaaaa;
-	background: #ffffff;
-	color: #222222;
-}
-.ui-widget-content a {
-	color: #222222;
-}
-.ui-widget-header {
-	border: 1px solid #aaaaaa;
-	background: #cccccc url("images/ui-bg_highlight-soft_75_cccccc_1x100.png") 50% 50% repeat-x;
-	color: #222222;
-	font-weight: bold;
-}
-.ui-widget-header a {
-	color: #222222;
-}
-
-/* Interaction states
-----------------------------------*/
-.ui-state-default,
-.ui-widget-content .ui-state-default,
-.ui-widget-header .ui-state-default,
-.ui-button,
-
-/* We use html here because we need a greater specificity to make sure disabled
-works properly when clicked or hovered */
-html .ui-button.ui-state-disabled:hover,
-html .ui-button.ui-state-disabled:active {
-	border: 1px solid #d3d3d3;
-	background: #e6e6e6 url("images/ui-bg_glass_75_e6e6e6_1x400.png") 50% 50% repeat-x;
-	font-weight: normal;
-	color: #555555;
-}
-.ui-state-default a,
-.ui-state-default a:link,
-.ui-state-default a:visited,
-a.ui-button,
-a:link.ui-button,
-a:visited.ui-button,
-.ui-button {
-	color: #555555;
-	text-decoration: none;
-}
-.ui-state-hover,
-.ui-widget-content .ui-state-hover,
-.ui-widget-header .ui-state-hover,
-.ui-state-focus,
-.ui-widget-content .ui-state-focus,
-.ui-widget-header .ui-state-focus,
-.ui-button:hover,
-.ui-button:focus {
-	border: 1px solid #999999;
-	background: #dadada url("images/ui-bg_glass_75_dadada_1x400.png") 50% 50% repeat-x;
-	font-weight: normal;
-	color: #212121;
-}
-.ui-state-hover a,
-.ui-state-hover a:hover,
-.ui-state-hover a:link,
-.ui-state-hover a:visited,
-.ui-state-focus a,
-.ui-state-focus a:hover,
-.ui-state-focus a:link,
-.ui-state-focus a:visited,
-a.ui-button:hover,
-a.ui-button:focus {
-	color: #212121;
-	text-decoration: none;
-}
-
-.ui-visual-focus {
-	box-shadow: 0 0 3px 1px rgb(94, 158, 214);
-}
-.ui-state-active,
-.ui-widget-content .ui-state-active,
-.ui-widget-header .ui-state-active,
-a.ui-button:active,
-.ui-button:active,
-.ui-button.ui-state-active:hover {
-	border: 1px solid #aaaaaa;
-	background: #ffffff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;
-	font-weight: normal;
-	color: #212121;
-}
-.ui-icon-background,
-.ui-state-active .ui-icon-background {
-	border: #aaaaaa;
-	background-color: #212121;
-}
-.ui-state-active a,
-.ui-state-active a:link,
-.ui-state-active a:visited {
-	color: #212121;
-	text-decoration: none;
-}
-
-/* Interaction Cues
-----------------------------------*/
-.ui-state-highlight,
-.ui-widget-content .ui-state-highlight,
-.ui-widget-header .ui-state-highlight {
-	border: 1px solid #fcefa1;
-	background: #fbf9ee url("images/ui-bg_glass_55_fbf9ee_1x400.png") 50% 50% repeat-x;
-	color: #363636;
-}
-.ui-state-checked {
-	border: 1px solid #fcefa1;
-	background: #fbf9ee;
-}
-.ui-state-highlight a,
-.ui-widget-content .ui-state-highlight a,
-.ui-widget-header .ui-state-highlight a {
-	color: #363636;
-}
-.ui-state-error,
-.ui-widget-content .ui-state-error,
-.ui-widget-header .ui-state-error {
-	border: 1px solid #cd0a0a;
-	background: #fef1ec url("images/ui-bg_glass_95_fef1ec_1x400.png") 50% 50% repeat-x;
-	color: #cd0a0a;
-}
-.ui-state-error a,
-.ui-widget-content .ui-state-error a,
-.ui-widget-header .ui-state-error a {
-	color: #cd0a0a;
-}
-.ui-state-error-text,
-.ui-widget-content .ui-state-error-text,
-.ui-widget-header .ui-state-error-text {
-	color: #cd0a0a;
-}
-.ui-priority-primary,
-.ui-widget-content .ui-priority-primary,
-.ui-widget-header .ui-priority-primary {
-	font-weight: bold;
-}
-.ui-priority-secondary,
-.ui-widget-content .ui-priority-secondary,
-.ui-widget-header .ui-priority-secondary {
-	opacity: .7;
-	filter:Alpha(Opacity=70); /* support: IE8 */
-	font-weight: normal;
-}
-.ui-state-disabled,
-.ui-widget-content .ui-state-disabled,
-.ui-widget-header .ui-state-disabled {
-	opacity: .35;
-	filter:Alpha(Opacity=35); /* support: IE8 */
-	background-image: none;
-}
-.ui-state-disabled .ui-icon {
-	filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */
-}
-
-/* Icons
-----------------------------------*/
-
-/* states and images */
-.ui-icon {
-	width: 16px;
-	height: 16px;
-}
-.ui-icon,
-.ui-widget-content .ui-icon {
-	background-image: url("images/ui-icons_222222_256x240.png");
-}
-.ui-widget-header .ui-icon {
-	background-image: url("images/ui-icons_222222_256x240.png");
-}
-.ui-state-hover .ui-icon,
-.ui-state-focus .ui-icon,
-.ui-button:hover .ui-icon,
-.ui-button:focus .ui-icon {
-	background-image: url("images/ui-icons_454545_256x240.png");
-}
-.ui-state-active .ui-icon,
-.ui-button:active .ui-icon {
-	background-image: url("images/ui-icons_454545_256x240.png");
-}
-.ui-state-highlight .ui-icon,
-.ui-button .ui-state-highlight.ui-icon {
-	background-image: url("images/ui-icons_2e83ff_256x240.png");
-}
-.ui-state-error .ui-icon,
-.ui-state-error-text .ui-icon {
-	background-image: url("images/ui-icons_cd0a0a_256x240.png");
-}
-.ui-button .ui-icon {
-	background-image: url("images/ui-icons_888888_256x240.png");
-}
-
-/* positioning */
-.ui-icon-blank { background-position: 16px 16px; }
-.ui-icon-caret-1-n { background-position: 0 0; }
-.ui-icon-caret-1-ne { background-position: -16px 0; }
-.ui-icon-caret-1-e { background-position: -32px 0; }
-.ui-icon-caret-1-se { background-position: -48px 0; }
-.ui-icon-caret-1-s { background-position: -65px 0; }
-.ui-icon-caret-1-sw { background-position: -80px 0; }
-.ui-icon-caret-1-w { background-position: -96px 0; }
-.ui-icon-caret-1-nw { background-position: -112px 0; }
-.ui-icon-caret-2-n-s { background-position: -128px 0; }
-.ui-icon-caret-2-e-w { background-position: -144px 0; }
-.ui-icon-triangle-1-n { background-position: 0 -16px; }
-.ui-icon-triangle-1-ne { background-position: -16px -16px; }
-.ui-icon-triangle-1-e { background-position: -32px -16px; }
-.ui-icon-triangle-1-se { background-position: -48px -16px; }
-.ui-icon-triangle-1-s { background-position: -65px -16px; }
-.ui-icon-triangle-1-sw { background-position: -80px -16px; }
-.ui-icon-triangle-1-w { background-position: -96px -16px; }
-.ui-icon-triangle-1-nw { background-position: -112px -16px; }
-.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
-.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
-.ui-icon-arrow-1-n { background-position: 0 -32px; }
-.ui-icon-arrow-1-ne { background-position: -16px -32px; }
-.ui-icon-arrow-1-e { background-position: -32px -32px; }
-.ui-icon-arrow-1-se { background-position: -48px -32px; }
-.ui-icon-arrow-1-s { background-position: -65px -32px; }
-.ui-icon-arrow-1-sw { background-position: -80px -32px; }
-.ui-icon-arrow-1-w { background-position: -96px -32px; }
-.ui-icon-arrow-1-nw { background-position: -112px -32px; }
-.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
-.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
-.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
-.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
-.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
-.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
-.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
-.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
-.ui-icon-arrowthick-1-n { background-position: 1px -48px; }
-.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
-.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
-.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
-.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
-.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
-.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
-.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
-.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
-.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
-.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
-.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
-.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
-.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
-.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
-.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
-.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
-.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
-.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
-.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
-.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
-.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
-.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
-.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
-.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
-.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
-.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
-.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
-.ui-icon-arrow-4 { background-position: 0 -80px; }
-.ui-icon-arrow-4-diag { background-position: -16px -80px; }
-.ui-icon-extlink { background-position: -32px -80px; }
-.ui-icon-newwin { background-position: -48px -80px; }
-.ui-icon-refresh { background-position: -64px -80px; }
-.ui-icon-shuffle { background-position: -80px -80px; }
-.ui-icon-transfer-e-w { background-position: -96px -80px; }
-.ui-icon-transferthick-e-w { background-position: -112px -80px; }
-.ui-icon-folder-collapsed { background-position: 0 -96px; }
-.ui-icon-folder-open { background-position: -16px -96px; }
-.ui-icon-document { background-position: -32px -96px; }
-.ui-icon-document-b { background-position: -48px -96px; }
-.ui-icon-note { background-position: -64px -96px; }
-.ui-icon-mail-closed { background-position: -80px -96px; }
-.ui-icon-mail-open { background-position: -96px -96px; }
-.ui-icon-suitcase { background-position: -112px -96px; }
-.ui-icon-comment { background-position: -128px -96px; }
-.ui-icon-person { background-position: -144px -96px; }
-.ui-icon-print { background-position: -160px -96px; }
-.ui-icon-trash { background-position: -176px -96px; }
-.ui-icon-locked { background-position: -192px -96px; }
-.ui-icon-unlocked { background-position: -208px -96px; }
-.ui-icon-bookmark { background-position: -224px -96px; }
-.ui-icon-tag { background-position: -240px -96px; }
-.ui-icon-home { background-position: 0 -112px; }
-.ui-icon-flag { background-position: -16px -112px; }
-.ui-icon-calendar { background-position: -32px -112px; }
-.ui-icon-cart { background-position: -48px -112px; }
-.ui-icon-pencil { background-position: -64px -112px; }
-.ui-icon-clock { background-position: -80px -112px; }
-.ui-icon-disk { background-position: -96px -112px; }
-.ui-icon-calculator { background-position: -112px -112px; }
-.ui-icon-zoomin { background-position: -128px -112px; }
-.ui-icon-zoomout { background-position: -144px -112px; }
-.ui-icon-search { background-position: -160px -112px; }
-.ui-icon-wrench { background-position: -176px -112px; }
-.ui-icon-gear { background-position: -192px -112px; }
-.ui-icon-heart { background-position: -208px -112px; }
-.ui-icon-star { background-position: -224px -112px; }
-.ui-icon-link { background-position: -240px -112px; }
-.ui-icon-cancel { background-position: 0 -128px; }
-.ui-icon-plus { background-position: -16px -128px; }
-.ui-icon-plusthick { background-position: -32px -128px; }
-.ui-icon-minus { background-position: -48px -128px; }
-.ui-icon-minusthick { background-position: -64px -128px; }
-.ui-icon-close { background-position: -80px -128px; }
-.ui-icon-closethick { background-position: -96px -128px; }
-.ui-icon-key { background-position: -112px -128px; }
-.ui-icon-lightbulb { background-position: -128px -128px; }
-.ui-icon-scissors { background-position: -144px -128px; }
-.ui-icon-clipboard { background-position: -160px -128px; }
-.ui-icon-copy { background-position: -176px -128px; }
-.ui-icon-contact { background-position: -192px -128px; }
-.ui-icon-image { background-position: -208px -128px; }
-.ui-icon-video { background-position: -224px -128px; }
-.ui-icon-script { background-position: -240px -128px; }
-.ui-icon-alert { background-position: 0 -144px; }
-.ui-icon-info { background-position: -16px -144px; }
-.ui-icon-notice { background-position: -32px -144px; }
-.ui-icon-help { background-position: -48px -144px; }
-.ui-icon-check { background-position: -64px -144px; }
-.ui-icon-bullet { background-position: -80px -144px; }
-.ui-icon-radio-on { background-position: -96px -144px; }
-.ui-icon-radio-off { background-position: -112px -144px; }
-.ui-icon-pin-w { background-position: -128px -144px; }
-.ui-icon-pin-s { background-position: -144px -144px; }
-.ui-icon-play { background-position: 0 -160px; }
-.ui-icon-pause { background-position: -16px -160px; }
-.ui-icon-seek-next { background-position: -32px -160px; }
-.ui-icon-seek-prev { background-position: -48px -160px; }
-.ui-icon-seek-end { background-position: -64px -160px; }
-.ui-icon-seek-start { background-position: -80px -160px; }
-/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
-.ui-icon-seek-first { background-position: -80px -160px; }
-.ui-icon-stop { background-position: -96px -160px; }
-.ui-icon-eject { background-position: -112px -160px; }
-.ui-icon-volume-off { background-position: -128px -160px; }
-.ui-icon-volume-on { background-position: -144px -160px; }
-.ui-icon-power { background-position: 0 -176px; }
-.ui-icon-signal-diag { background-position: -16px -176px; }
-.ui-icon-signal { background-position: -32px -176px; }
-.ui-icon-battery-0 { background-position: -48px -176px; }
-.ui-icon-battery-1 { background-position: -64px -176px; }
-.ui-icon-battery-2 { background-position: -80px -176px; }
-.ui-icon-battery-3 { background-position: -96px -176px; }
-.ui-icon-circle-plus { background-position: 0 -192px; }
-.ui-icon-circle-minus { background-position: -16px -192px; }
-.ui-icon-circle-close { background-position: -32px -192px; }
-.ui-icon-circle-triangle-e { background-position: -48px -192px; }
-.ui-icon-circle-triangle-s { background-position: -64px -192px; }
-.ui-icon-circle-triangle-w { background-position: -80px -192px; }
-.ui-icon-circle-triangle-n { background-position: -96px -192px; }
-.ui-icon-circle-arrow-e { background-position: -112px -192px; }
-.ui-icon-circle-arrow-s { background-position: -128px -192px; }
-.ui-icon-circle-arrow-w { background-position: -144px -192px; }
-.ui-icon-circle-arrow-n { background-position: -160px -192px; }
-.ui-icon-circle-zoomin { background-position: -176px -192px; }
-.ui-icon-circle-zoomout { background-position: -192px -192px; }
-.ui-icon-circle-check { background-position: -208px -192px; }
-.ui-icon-circlesmall-plus { background-position: 0 -208px; }
-.ui-icon-circlesmall-minus { background-position: -16px -208px; }
-.ui-icon-circlesmall-close { background-position: -32px -208px; }
-.ui-icon-squaresmall-plus { background-position: -48px -208px; }
-.ui-icon-squaresmall-minus { background-position: -64px -208px; }
-.ui-icon-squaresmall-close { background-position: -80px -208px; }
-.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
-.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
-.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
-.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
-.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
-.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
-
-
-/* Misc visuals
-----------------------------------*/
-
-/* Corner radius */
-.ui-corner-all,
-.ui-corner-top,
-.ui-corner-left,
-.ui-corner-tl {
-	border-top-left-radius: 4px;
-}
-.ui-corner-all,
-.ui-corner-top,
-.ui-corner-right,
-.ui-corner-tr {
-	border-top-right-radius: 4px;
-}
-.ui-corner-all,
-.ui-corner-bottom,
-.ui-corner-left,
-.ui-corner-bl {
-	border-bottom-left-radius: 4px;
-}
-.ui-corner-all,
-.ui-corner-bottom,
-.ui-corner-right,
-.ui-corner-br {
-	border-bottom-right-radius: 4px;
-}
-
-/* Overlays */
-.ui-widget-overlay {
-	background: #aaaaaa;
-	opacity: .3;
-	filter: Alpha(Opacity=30); /* support: IE8 */
-}
-.ui-widget-shadow {
-	-webkit-box-shadow: -8px -8px 8px #aaaaaa;
-	box-shadow: -8px -8px 8px #aaaaaa;
-}
diff --git a/platypush/backend/http/static/css/map.css b/platypush/backend/http/static/css/map.css
deleted file mode 100644
index b23160e8..00000000
--- a/platypush/backend/http/static/css/map.css
+++ /dev/null
@@ -1,9 +0,0 @@
-#map {
-  width: 100%;
-  height: 100%;
-  position: static !important;
-  margin: 0;
-  padding: 0;
-  overflow: hidden;
-}
-
diff --git a/platypush/backend/http/static/css/media.css b/platypush/backend/http/static/css/media.css
deleted file mode 100644
index f0959643..00000000
--- a/platypush/backend/http/static/css/media.css
+++ /dev/null
@@ -1,180 +0,0 @@
-#video-search {
-    max-width: 60em;
-    margin: 1em auto;
-}
-
-    #video-search input[type=text] {
-        width: 100%;
-    }
-
-form#video-ctrl {
-    text-align: center;
-}
-    form#video-ctrl * > button[data-modal="#media-subtitles-modal"] {
-        display: none;
-    }
-
-#video-seeker-container {
-    margin-top: 0.5em;
-    margin-bottom: 1em;
-}
-
-#video-volume-ctrl-container {
-    margin-top: 1em;
-}
-
-#video-results {
-    padding: 1.5rem 1.5rem 0 .5rem;
-    background: #f8f8f8;
-}
-
-    .video-result {
-        padding: 5px;
-        letter-spacing: .1rem;
-        line-height: 3.3rem;
-        cursor: pointer;
-    }
-
-    .video-icon-container {
-        font-size: 2rem;
-        margin-right: 2rem;
-    }
-
-        .video-result.selected {
-            background-color: #c8ffd0 !important;
-        }
-
-        .video-result:hover {
-            background-color: #daf8e2 !important;
-        }
-
-        .video-result:nth-child(odd) {
-            background-color: #f2f2f2;
-        }
-
-        .video-result.active {
-            height: 4rem;
-            padding-top: 1.5rem;
-            font-size: 1.7rem;
-            border-radius: 10px;
-            animation: active-track 5s;
-            -moz-animation: active-track 5s infinite;
-            -webkit-animation: active-track 5s infinite;
-        }
-
-            @keyframes active-track {
-                0% { background: #d4ffe3; }
-                50% { background: #9cdfb0; }
-                100% { background: #d4ffe3; }
-            }
-
-            @-moz-keyframes active-track {
-                0% { background: #d4ffe3; }
-                50% { background: #9cdfb0; }
-                100% { background: #d4ffe3; }
-            }
-
-            @-webkit-keyframes active-track {
-                0% { background: #d4ffe3; }
-                50% { background: #9cdfb0; }
-                100% { background: #d4ffe3; }
-            }
-
-    button.remote[data-panel="#media-devices-panel"] {
-        color: #34b868;
-    }
-
-#media-devices-panel,
-#media-item-panel {
-    display: none;
-    position: absolute;
-    padding: 1rem;
-    background: #f0f0f0;
-    z-index: 10;
-    border: 1px solid #d0d0d0;
-    border-radius: 5px;
-    min-width: 10em;
-}
-
-    #media-devices-panel .refresh-devices-container {
-        position: relative;
-        height: 1em;
-    }
-
-    #media-devices-panel * > .refresh-devices {
-        text-align: right;
-        cursor: pointer;
-        position: absolute;
-        top: -.5rem;
-        right: .5rem;
-    }
-
-    #media-devices-panel * > .cast-device,
-    #media-item-panel > .media-item-action {
-        padding: 0.5rem;
-        cursor: pointer;
-    }
-
-    #media-devices-panel * > .cast-device-local {
-        border-bottom: 1px solid #ddd;
-        margin-bottom: 0.25rem;
-    }
-
-        #media-devices-panel * > .cast-device.selected {
-            font-weight: bold;
-            color: #34b868;
-        }
-
-        #media-devices-panel * > .cast-device.disabled,
-        #media-devices-panel * > .refresh-devices.disabled,
-        #media-item-panel > .media-item-action.disabled {
-            cursor: default;
-            color: #999 !important;
-        }
-
-        #media-devices-panel * > .cast-device:hover,
-        #media-item-panel > .media-item-action:hover {
-            background-color: #daf8e2 !important;
-        }
-
-        #media-devices-panel * > .cast-device-icon
-        #media-item-panel > .media-item-item {
-            color: #666;
-        }
-
-    #media-subtitles-modal * > .media-subtitles-results-container {
-        display: none;
-        padding: .75rem;
-    }
-
-    #media-subtitles-modal * > .media-subtitles-results-header {
-        background: #eee;
-        margin-bottom: 1rem;
-        padding: 1rem .25rem;
-        border: 1px solid #ccc;
-    }
-
-    #media-subtitles-modal * > .media-subtitles-results {
-        padding: .75rem;
-    }
-
-    #media-subtitles-modal * > .media-subtitles-message {
-        display: none;
-    }
-
-        #media-subtitles-modal * > .media-subtitle-container {
-            cursor: pointer;
-        }
-
-            #media-subtitles-modal * > .media-subtitle-container:nth-child(odd) {
-                background-color: #f2f2f2;
-            }
-
-            #media-subtitles-modal * > .media-subtitle-container.selected {
-                background-color: #c8ffd0 !important;
-            }
-
-            #media-subtitles-modal * > .media-subtitle-container:hover {
-                background-color: #daf8e2 !important;
-            }
-
diff --git a/platypush/backend/http/static/css/music.mpd.css b/platypush/backend/http/static/css/music.mpd.css
deleted file mode 100644
index 7c8def7b..00000000
--- a/platypush/backend/http/static/css/music.mpd.css
+++ /dev/null
@@ -1,173 +0,0 @@
-#player-left-side {
-    overflow-y: hidden;
-}
-
-#player-right-side {
-    margin-left: 0;
-    width: 78%;
-}
-
-    .playback-controls {
-        text-align: center;
-        border-bottom: 1px solid #e8eaf0;
-        padding-bottom: 12px;
-    }
-
-        .playback-controls * > button.enabled {
-            color: #59df3e;
-        }
-
-
-    .track-info {
-        text-align: center;
-        margin: -20px -20px 0 -20px;
-        padding: 10px 20px;
-    }
-
-        .track-info > .artist {
-            font-weight: bold;
-            display: block;
-        }
-
-#playlist-controls, #browser-controls {
-    margin-bottom: 7.5px;
-    padding-bottom: 7.5px;
-    border-bottom: 1px solid #ddd;
-    height: 3.8rem;
-}
-
-#playlist-controls {
-    text-align: right;
-}
-
-#playlist-content, #music-browser {
-    height: 27.2rem;
-    overflow-y: scroll;
-}
-
-#music-browser {
-    padding-top: 0;
-}
-
-#browser-filter {
-    width: 95%;
-    margin-bottom: 1.5rem;
-}
-
-#playlist-filter-container {
-    height: 5rem;
-}
-
-#playlist-filter {
-    width: 100%;
-    margin-bottom: 1.5rem;
-}
-
-.music-pane {
-    height: 40rem;
-    padding: 15px 15px 0 5px;
-    background: #f8f8f8;
-}
-
-    .music-item {
-        padding: 5px;
-        cursor: pointer;
-    }
-
-        .music-item.selected {
-            background-color: #c8ffd0 !important;
-        }
-
-        .music-item:hover {
-            background-color: #daf8e2 !important;
-        }
-
-        .music-item:nth-child(odd) {
-            background-color: #f2f2f2;
-        }
-
-            .playlist-track.active {
-                height: 4rem;
-                padding-top: 1.5rem;
-                font-size: 1.7rem;
-                border-radius: 10px;
-                animation: active-track 5s;
-                -moz-animation: active-track 5s infinite;
-                -webkit-animation: active-track 5s infinite;
-            }
-
-            @keyframes active-track {
-                0% { background: #d4ffe3; }
-                50% { background: #9cdfb0; }
-                100% { background: #d4ffe3; }
-            }
-
-            @-moz-keyframes active-track {
-                0% { background: #d4ffe3; }
-                50% { background: #9cdfb0; }
-                100% { background: #d4ffe3; }
-            }
-
-            @-webkit-keyframes active-track {
-                0% { background: #d4ffe3; }
-                50% { background: #9cdfb0; }
-                100% { background: #d4ffe3; }
-            }
-
-        .playlist-track > .track-time {
-            text-align: right;
-            color: #666;
-        }
-
-#track-seeker-container {
-    margin-bottom: 10px;
-}
-
-#volume-ctrl-container {
-    margin-top: 15px;
-}
-
-#music-search-form {
-    margin-bottom: 1rem;
-}
-
-    #music-search-form * > input[type=text] {
-        width: 100%;
-    }
-
-    #music-search-form > .row {
-        padding: 0.5rem;
-    }
-
-    .music-form-bottom {
-        text-align: right;
-        margin-top: 2rem;
-        border-top: 1px solid #ddd;
-    }
-
-        .music-form-bottom input {
-            margin-top: 2rem;
-        }
-
-#music-search-results-form {
-    display: none;
-    margin-top: -2rem;
-}
-
-#music-search-results-container {
-    display: none;
-    max-height: 50rem;
-    margin-top: -1.4rem;
-    overflow-y: auto;
-    overflow-x: hidden;
-}
-
-    #music-search-results-head {
-        padding: 0.5rem 1rem;
-        background-color: #eaeaea;
-        border-radius: 5px 5px 0 0;
-        border-bottom: 1px solid #bbb;
-        letter-spacing: .1rem;
-        line-height: 3.5rem;
-    }
-
diff --git a/platypush/backend/http/static/css/music.snapcast.css b/platypush/backend/http/static/css/music.snapcast.css
deleted file mode 100644
index 5cb18d1d..00000000
--- a/platypush/backend/http/static/css/music.snapcast.css
+++ /dev/null
@@ -1,153 +0,0 @@
-.snapcast-host-container {
-    min-width: 40em;
-    max-width: 80em;
-    margin: 1em auto;
-    background: rgba(245,245,245,0.6);
-    border: 1px solid rgba(220,220,220,1.0);
-    border-radius: 10px;
-}
-
-.snapcast-host-header {
-    border-bottom: 1px solid rgba(220,220,220,1.0);
-    font-size: 1em;
-    text-transform: uppercase;
-    padding: 0 .5em;
-}
-
-    .snapcast-host-header h1 {
-        font-size: 1.9em;
-    }
-
-.snapcast-group-header {
-    padding: .5em;
-    margin: 0 1.4em 1.8em .2em;
-    border-bottom: 1px solid #e8e8e8;
-}
-
-    .snapcast-settings-btn {
-        cursor: pointer;
-        padding: .2em;
-    }
-
-        .snapcast-settings-btn:hover {
-            border: .05em solid #e0e0e0;
-            padding: .15em;
-        }
-
-    .snapcast-group-header h2 {
-        font-size: 1.5em;
-        margin-top: .6em;
-    }
-
-.snapcast-client-disconnected {
-    color: rgba(0, 0, 0, 0.35);
-}
-
-.snapcast-client-row {
-    padding: 0 1.4em;
-    margin: 1.5em .5em;
-}
-
-    .snapcast-client-row h3 {
-        font-size: 1.2em;
-    }
-
-.snapcast-client-mute-toggle {
-    margin-top: -1.2em;
-}
-
-.snapcast-form {
-    margin-bottom: 1rem;
-}
-
-    .snapcast-form * > input[type=text] {
-        width: 100%;
-    }
-
-    .snapcast-form > .row {
-        padding: 0.5rem;
-    }
-
-    .snapcast-form-bottom {
-        text-align: right;
-        margin-top: 2rem;
-        border-top: 1px solid #ddd;
-    }
-
-        .snapcast-form-bottom input {
-            margin-top: 2rem;
-        }
-
-    .snapcast-form * > label {
-        transform: translateY(25%);
-    }
-
-    .snapcast-form > .snapcast-client-info,
-    .snapcast-form > .snapcast-host-info,
-    .snapcast-form > .snapcast-group-clients-container {
-        margin: 1em auto .5em auto;
-        padding: 2em;
-        background: rgba(240,240,240,0.6);
-        border-radius: 10px;
-        border: .05em solid rgba(225,225,225,1.0)
-    }
-
-    .snapcast-form > .snapcast-group-clients-container {
-        max-width: 40em;
-    }
-
-    .snapcast-form > .snapcast-host-info,
-    .snapcast-form > .snapcast-client-info {
-        max-width: 70%;
-    }
-
-        .snapcast-form > .snapcast-host-info > .row,
-        .snapcast-form > .snapcast-client-info > .row {
-            padding: .2em;
-        }
-
-            .snapcast-form > .snapcast-host-info > .row:hover,
-            .snapcast-form > .snapcast-client-info > .row:hover {
-                background-color: #daf8e2 !important;
-            }
-
-        .snapcast-form > .snapcast-host-info * > .info-name,
-        .snapcast-form > .snapcast-client-info * > .info-name {
-            font-weight: bold;
-        }
-
-        .snapcast-form > .snapcast-host-info * > .info-value,
-        .snapcast-form > .snapcast-client-info * > .info-value {
-            text-align: right;
-        }
-
-        .snapcast-form > .snapcast-group-stream,
-        .snapcast-form > .snapcast-client-delete {
-            width: 30%;
-            margin: 1em auto 0 auto;
-            text-align: center;
-        }
-
-            .snapcast-form > .snapcast-group-stream > label,
-            .snapcast-form > .snapcast-client-delete > label {
-                display: inline;
-                margin-left: .5em;
-                text-align: right;
-            }
-
-            .snapcast-form > .snapcast-client-delete > label {
-                color: rgba(200, 44, 23, 1.0);
-            }
-
-            .snapcast-form * > .snapcast-group-clients {
-                max-width: 15em;
-                margin: auto;
-                text-align: right;
-            }
-
-                .snapcast-form * > .snapcast-group-clients * > label {
-                    display: inline;
-                    float: right;
-                    width: 80%;
-                }
-
diff --git a/platypush/backend/http/static/css/source/dashboard/index.scss b/platypush/backend/http/static/css/source/dashboard/index.scss
new file mode 100644
index 00000000..79f7e82f
--- /dev/null
+++ b/platypush/backend/http/static/css/source/dashboard/index.scss
@@ -0,0 +1,49 @@
+@import 'common/vars';
+
+@import 'common/mixins';
+@import 'common/layout';
+@import 'common/elements';
+@import 'common/animations';
+@import 'common/modal';
+@import 'common/notifications';
+
+$background-image: url('/img/dashboard-background.jpg') !default;
+$background-color: white !default;
+$font-family: Lato !default;
+
+html {
+    min-height: 100%;
+}
+
+body {
+    --background-image: $background-image;
+    --background-color: $background-color;
+
+    background-image: var(--background-image);
+    background-color: var(--background-color);
+    background-size: 100% 100%;
+    background-repeat: no-repeat;
+    font-family: $font-family;
+}
+
+main {
+    display: flex;
+    flex-flow: column;
+    width: 100%;
+    height: 100%;
+    margin: 0;
+
+    .widgets-row {
+        margin: 2em 1em;
+        display: flex;
+    }
+
+    .widget {
+        background: $background-color;
+        border-radius: 5px;
+        height: 22.5em;
+        overflow: hidden;
+        box-shadow: 0 3px 3px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08);
+    }
+}
+
diff --git a/platypush/backend/http/static/css/source/dashboard/widgets/calendar/index.scss b/platypush/backend/http/static/css/source/dashboard/widgets/calendar/index.scss
new file mode 100644
index 00000000..57f37753
--- /dev/null
+++ b/platypush/backend/http/static/css/source/dashboard/widgets/calendar/index.scss
@@ -0,0 +1,16 @@
+@import 'common/vars';
+
+.widget .calendar {
+    padding: 1rem;
+
+    .upcoming-event {
+        text-align: center;
+        margin-bottom: 1.5rem;
+
+        .summary {
+            text-transform: uppercase;
+            font-size: 1.35em;
+        }
+    }
+}
+
diff --git a/platypush/backend/http/static/css/source/dashboard/widgets/date-time-weather/index.scss b/platypush/backend/http/static/css/source/dashboard/widgets/date-time-weather/index.scss
new file mode 100644
index 00000000..c2713883
--- /dev/null
+++ b/platypush/backend/http/static/css/source/dashboard/widgets/date-time-weather/index.scss
@@ -0,0 +1,48 @@
+@import 'common/vars';
+
+.widget .date-time-weather {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding-top: 1rem;
+
+    .date {
+        font-size: 1.15em;
+        height: 8%;
+    }
+
+    .time {
+        font-size: 1.7em;
+        height: 11%;
+    }
+
+    .weather {
+        height: 40%;
+        display: flex;
+        align-items: center;
+
+        .temperature {
+            font-size: 2em;
+            margin-left: 1rem;
+        }
+    }
+
+    .summary {
+        height: 28%;
+    }
+
+    .sensors {
+        width: 100%;
+        height: 13%;
+
+        .sensor {
+            padding: 0 1rem;
+        }
+
+        .humidity {
+            text-align: right;
+        }
+    }
+}
+
diff --git a/platypush/backend/http/static/css/source/dashboard/widgets/image-carousel/index.scss b/platypush/backend/http/static/css/source/dashboard/widgets/image-carousel/index.scss
new file mode 100644
index 00000000..346aa94d
--- /dev/null
+++ b/platypush/backend/http/static/css/source/dashboard/widgets/image-carousel/index.scss
@@ -0,0 +1,30 @@
+@import 'common/vars';
+
+.widget .image-carousel {
+    height: 100%;
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background-color: rgba(0,0,0,0);
+
+    .background {
+        position: absolute;
+        top: 0;
+        width: 100%;
+        height: 100vh;
+        background-color: rgba(0,0,0,0);
+        background-position: center;
+        background-size: cover;
+        background-repeat: no-repeat;
+        filter: blur(13px);
+        -webkit-filter: blur(13px);
+    }
+
+    img {
+        position: absolute;
+        max-height: 100%;
+        z-index: 2;
+    }
+}
+
diff --git a/platypush/backend/http/static/css/source/dashboard/widgets/music/index.scss b/platypush/backend/http/static/css/source/dashboard/widgets/music/index.scss
new file mode 100644
index 00000000..6d043c61
--- /dev/null
+++ b/platypush/backend/http/static/css/source/dashboard/widgets/music/index.scss
@@ -0,0 +1,92 @@
+@import 'common/vars';
+
+$progress-bar-bg: #ddd;
+$playback-status-color: #757f70;
+
+.widget .music {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+
+    .track {
+        text-align: center;
+
+        .unknown,
+        .no-track {
+            font-size: 1.5em;
+        }
+
+        .artist {
+            font-size: 1.4em;
+            font-weight: bold;
+            margin-bottom: .25em;
+        }
+
+        .title {
+            font-size: 1.25em;
+        }
+    }
+
+    .time {
+        width: 100%;
+        margin-top: .75em;
+
+        .row {
+            padding: 0 .5em;
+        }
+
+        .time-total {
+            text-align: right;
+        }
+
+        .progress-bar {
+            width: 100%;
+            height: 1em;
+            position: relative;
+            margin-bottom: .75em;
+
+            .total {
+                position: absolute;
+                width: 100%;
+                height: 100%;
+                top: 0;
+                background: $progress-bar-bg;
+                border-radius: 5rem;
+            }
+
+            .elapsed {
+                position: absolute;
+                width: 100%;
+                height: 100%;
+                top: 0;
+                background: $selected-bg;
+                border-radius: 5rem;
+                z-index: 1;
+            }
+        }
+    }
+
+    .playback-status {
+        position: absolute;
+        bottom: 0;
+        border-top: $default-border-2;
+        color: $playback-status-color;
+        width: 100%;
+        height: 2em;
+
+        .status-property {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            height: 100%;
+        }
+
+        .active {
+            color: $default-hover-fg;
+        }
+    }
+}
+
diff --git a/platypush/backend/http/static/css/source/dashboard/widgets/rss-news/index.scss b/platypush/backend/http/static/css/source/dashboard/widgets/rss-news/index.scss
new file mode 100644
index 00000000..53b38324
--- /dev/null
+++ b/platypush/backend/http/static/css/source/dashboard/widgets/rss-news/index.scss
@@ -0,0 +1,29 @@
+@import 'common/vars';
+
+.widget .rss-news {
+    height: 100%;
+    display: flex;
+    align-items: center;
+
+    .article {
+        width: 100%;
+        padding: 0 2em;
+
+        .source {
+            font-size: 1.25em;
+            font-weight: bold;
+            margin-bottom: .5em;
+        }
+
+        .title {
+            font-size: 1.25em;
+            margin-bottom: .5em;
+        }
+
+        .published {
+            text-align: right;
+            font-size: .9em;
+        }
+    }
+}
+
diff --git a/platypush/backend/http/static/css/switch.switchbot.css b/platypush/backend/http/static/css/switch.switchbot.css
deleted file mode 100644
index d0367404..00000000
--- a/platypush/backend/http/static/css/switch.switchbot.css
+++ /dev/null
@@ -1,29 +0,0 @@
-#switchbot-container {
-    background-color: #f8f8f8;
-    padding: 12px;
-    border: 1px solid #ddd;
-    border-radius: 10px;
-    font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
-    font-weight: 400;
-    line-height: 38px;
-    letter-spacing: .1rem;
-}
-
-.switchbot-device {
-    padding: 1.5em 1em .2em .5em;
-    border-radius: 10px;
-}
-
-    .switchbot-device:nth-of-type(odd) {
-        background-color: #ececec;
-    }
-
-    .switchbot-device:hover {
-        background-color: #daf8e2 !important;
-    }
-
-.toggle-container {
-    text-align: right;
-    padding-right: 1em;
-}
-
diff --git a/platypush/backend/http/static/css/switch.tplink.css b/platypush/backend/http/static/css/switch.tplink.css
deleted file mode 100644
index 4ab62f91..00000000
--- a/platypush/backend/http/static/css/switch.tplink.css
+++ /dev/null
@@ -1,29 +0,0 @@
-#tplink-container {
-    background-color: #f8f8f8;
-    padding: 12px;
-    border: 1px solid #ddd;
-    border-radius: 10px;
-    font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
-    font-weight: 400;
-    line-height: 38px;
-    letter-spacing: .1rem;
-}
-
-.tplink-device {
-    padding: 1.5em 1em .2em .5em;
-    border-radius: 10px;
-}
-
-    .tplink-device:nth-of-type(odd) {
-        background-color: #ececec;
-    }
-
-    .tplink-device:hover {
-        background-color: #daf8e2 !important;
-    }
-
-.toggle-container {
-    text-align: right;
-    padding-right: 1em;
-}
-
diff --git a/platypush/backend/http/static/css/switch.wemo.css b/platypush/backend/http/static/css/switch.wemo.css
deleted file mode 100644
index ba849c87..00000000
--- a/platypush/backend/http/static/css/switch.wemo.css
+++ /dev/null
@@ -1,29 +0,0 @@
-#wemo-container {
-    background-color: #f8f8f8;
-    padding: 12px;
-    border: 1px solid #ddd;
-    border-radius: 10px;
-    font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
-    font-weight: 400;
-    line-height: 38px;
-    letter-spacing: .1rem;
-}
-
-.wemo-device {
-    padding: 1.5em 1em .2em .5em;
-    border-radius: 10px;
-}
-
-    .wemo-device:nth-of-type(odd) {
-        background-color: #ececec;
-    }
-
-    .wemo-device:hover {
-        background-color: #daf8e2 !important;
-    }
-
-.toggle-container {
-    text-align: right;
-    padding-right: 1em;
-}
-
diff --git a/platypush/backend/http/static/css/tts.css b/platypush/backend/http/static/css/tts.css
deleted file mode 100644
index 2714662e..00000000
--- a/platypush/backend/http/static/css/tts.css
+++ /dev/null
@@ -1,9 +0,0 @@
-#tts-container {
-    max-width: 60em;
-    margin: 3em auto;
-}
-
-    #tts-form input[type=text] {
-        width: 100%;
-    }
-
diff --git a/platypush/backend/http/static/css/widgets/calendar.css b/platypush/backend/http/static/css/widgets/calendar.css
deleted file mode 100644
index a01415ea..00000000
--- a/platypush/backend/http/static/css/widgets/calendar.css
+++ /dev/null
@@ -1,30 +0,0 @@
-.calendar-container {
-    padding: 2rem;
-    overflow: hidden;
-}
-
-    .calendar-container * > .date,
-    .calendar-container * > .time {
-        font-size: 0.9em;
-    }
-
-.calendar-next-event-container {
-    width: 70%;
-    margin: auto;
-    text-align: center;
-}
-
-    .calendar-next-event-container > .summary {
-        font-size: 1.5em;
-        text-transform: uppercase;
-    }
-
-.calendar-events-list-container {
-    margin-top: 2.5rem;
-}
-
-    .calendar-event > .date,
-    .calendar-event > .time {
-        padding-top: .15rem;
-    }
-
diff --git a/platypush/backend/http/static/css/widgets/date-time-weather.css b/platypush/backend/http/static/css/widgets/date-time-weather.css
deleted file mode 100644
index 47f6c229..00000000
--- a/platypush/backend/http/static/css/widgets/date-time-weather.css
+++ /dev/null
@@ -1,50 +0,0 @@
-.widget.date-time-weather {
-    text-align: center;
-}
-
-.date-time-weather-container {
-    padding: 2rem;
-    position: relative;
-    min-height: 20.5em;
-}
-
-#weather-icon {
-    margin-left: 2rem;
-}
-
-h1.temperature {
-    font-size: 45px;
-    margin: 4rem 2rem;
-    font-weight: bold;
-}
-
-    h1.temperature > [data-bind=temperature] {
-        margin-left: -2.5rem;
-    }
-
-    .widget.date-time-weather * > .time {
-        font-size: 22px;
-    }
-
-    .widget.date-time-weather * > .sensors {
-        position: absolute;
-        bottom: 2rem;
-        width: 90%;
-        margin-left: -2rem;
-        padding-left: 1rem;
-    }
-
-    .widget.date-time-weather * > .sensor-temperature,
-    .widget.date-time-weather * > .sensor-humidity {
-        display: none;
-        font-weight: bold;
-    }
-
-    .widget.date-time-weather * > .sensor-temperature {
-        text-align: left;
-    }
-
-    .widget.date-time-weather * > .sensor-humidity {
-        text-align: right;
-    }
-
diff --git a/platypush/backend/http/static/css/widgets/image-carousel.css b/platypush/backend/http/static/css/widgets/image-carousel.css
deleted file mode 100644
index ced953ea..00000000
--- a/platypush/backend/http/static/css/widgets/image-carousel.css
+++ /dev/null
@@ -1,33 +0,0 @@
-.carousel {
-    overflow: hidden;
-    color: #999;
-    position: relative;
-    width: 100%;
-    height: 22.5em;
-}
-
-.carousel > img {
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    margin-left: auto;
-    margin-right: auto;
-    border-radius: 5px;
-    z-index: 2;
-}
-
-.carousel-background {
-    position: absolute;
-    width: 100%;
-    height: 22.5em;
-    top: 0;
-    left: 0;
-    background-color: black;
-    background-size: 100% 100%;
-    background-position: center;
-    background-repeat: no-repeat;
-    filter: blur(10px);
-    z-index: 1;
-}
-
diff --git a/platypush/backend/http/static/css/widgets/music.css b/platypush/backend/http/static/css/widgets/music.css
deleted file mode 100644
index 126c625f..00000000
--- a/platypush/backend/http/static/css/widgets/music.css
+++ /dev/null
@@ -1,68 +0,0 @@
-.music-container {
-    position: relative;
-    width: inherit;
-    height: inherit;
-    display: table-cell;
-    vertical-align: middle;
-    text-align: center;
-}
-
-.track-info {
-    font-size: 20px;
-}
-
-    .track-info > .artist {
-        font-weight: bold;
-        margin-bottom: 1rem;
-    }
-
-    .track-info > .title {
-        margin-bottom: 2rem;
-    }
-
-.time-bar {
-    height: 7.5px;
-    background: #ddd;
-    margin: 7.5px;
-    border-radius: 10px;
-}
-
-    .time-bar > .elapsed {
-        height: 7.5px;
-        background: #98ffb0;
-        border-radius: 10px;
-    }
-
-.time-elapsed {
-    text-align: left;
-    margin-left: 1rem;
-}
-
-.time-total {
-    float: right;
-    margin-right: 1rem;
-}
-
-.time-elapsed, .time-total {
-    color: rgba(0, 0, 0, 0.7);
-    letter-spacing: 1px;
-}
-
-.no-track-info {
-    font-size: 25px;
-}
-
-.playback-status-container {
-    position: absolute;
-    bottom: 0;
-    width: 100%;
-    border-top: 1px solid #ddd;
-    padding: 1rem 0;
-    font-size: 1.22rem;
-    color: #757f70;
-}
-
-    .playback-status-container > .playback-status-values {
-        font-weight: bold;
-    }
-
diff --git a/platypush/backend/http/static/css/widgets/rss-news.css b/platypush/backend/http/static/css/widgets/rss-news.css
deleted file mode 100644
index 6c5e1afc..00000000
--- a/platypush/backend/http/static/css/widgets/rss-news.css
+++ /dev/null
@@ -1,28 +0,0 @@
-.news-container {
-    position: relative;
-    height: 100%;
-}
-
-    .news-container > .article {
-        width: 80%;
-        position: absolute;
-        top: 50%;
-        left: 50%;
-        transform: translate(-50%, -50%);
-    }
-
-        .news-container > .article > .source {
-            font-weight: bold;
-            margin-bottom: 1rem;
-            font-size: 1.7em;
-        }
-
-        .news-container > .article > .title {
-            font-size: 1.5em;
-        }
-
-        .news-container > .article > .publish-time {
-            margin-top: 1rem;
-            float: right;
-        }
-
diff --git a/platypush/backend/http/static/js/dashboard.js b/platypush/backend/http/static/js/dashboard.js
index 63b26218..1bba3fe0 100644
--- a/platypush/backend/http/static/js/dashboard.js
+++ b/platypush/backend/http/static/js/dashboard.js
@@ -1,46 +1,75 @@
-$(document).ready(function() {
-    var onEvent = function(event) {
-        if (event.args.type == 'platypush.message.event.web.widget.WidgetUpdateEvent') {
-            var $widget = $('#' + event.args.widget);
-            delete event.args.widget;
-
-            for (var key of Object.keys(event.args)) {
-                $widget.find('[data-bind=' + key + ']').text(event.args[key]);
-            }
-        } else if (event.args.type == 'platypush.message.event.web.DashboardIframeUpdateEvent') {
-            var url = event.args.url;
-            var $modal = $('#iframe-modal');
-            var $iframe = $modal.find('iframe');
-            $iframe.attr('src', url);
-            $iframe.prop('width',  event.args.width  || '100%');
-            $iframe.prop('height', event.args.height || '600');
-
-            if ('timeout' in event.args) {
-                setTimeout(function() {
-                    $iframe.removeAttr('src');
-                    $modal.fadeOut();
-                }, parseFloat(event.args.timeout) * 1000);
-            }
-
-            $modal.fadeIn();
-        }
-    };
-
-    var initDashboard = function() {
-        if ('background_image' in window.config) {
-            $('body').css('background-image', 'url(' + window.config.background_image + ')');
-        }
-    };
-
-    var initEvents = function() {
-        window.registerEventListener(onEvent);
-    };
-
-    var init = function() {
-        initDashboard();
-        initEvents();
-    };
-
-    init();
+Vue.component('widget', {
+    template: '#tmpl-widget',
+    props: ['config','tag'],
 });
 
+// Declaration of the main vue app
+window.vm = new Vue({
+    el: '#app',
+
+    props: {
+        config: {
+            type: Object,
+            default: () => window.config,
+        },
+    },
+
+    data: function() {
+        return {
+            iframeModal: {
+                visible: false,
+            },
+        };
+    },
+
+    created: function() {
+        initEvents();
+    },
+});
+
+// $(document).ready(function() {
+//     var onEvent = function(event) {
+//         if (event.args.type == 'platypush.message.event.web.widget.WidgetUpdateEvent') {
+//             var $widget = $('#' + event.args.widget);
+//             delete event.args.widget;
+
+//             for (var key of Object.keys(event.args)) {
+//                 $widget.find('[data-bind=' + key + ']').text(event.args[key]);
+//             }
+//         } else if (event.args.type == 'platypush.message.event.web.DashboardIframeUpdateEvent') {
+//             var url = event.args.url;
+//             var $modal = $('#iframe-modal');
+//             var $iframe = $modal.find('iframe');
+//             $iframe.attr('src', url);
+//             $iframe.prop('width',  event.args.width  || '100%');
+//             $iframe.prop('height', event.args.height || '600');
+
+//             if ('timeout' in event.args) {
+//                 setTimeout(function() {
+//                     $iframe.removeAttr('src');
+//                     $modal.fadeOut();
+//                 }, parseFloat(event.args.timeout) * 1000);
+//             }
+
+//             $modal.fadeIn();
+//         }
+//     };
+
+//     var initDashboard = function() {
+//         if (window.config.dashboard.background_image) {
+//             $('body').css('background-image', 'url(' + window.config.dashboard.background_image + ')');
+//         }
+//     };
+
+//     var initEvents = function() {
+//         registerEventListener(onEvent);
+//     };
+
+//     var init = function() {
+//         initDashboard();
+//         initEvents();
+//     };
+
+//     init();
+// });
+
diff --git a/platypush/backend/http/static/js/dashboard_old.js b/platypush/backend/http/static/js/dashboard_old.js
new file mode 100644
index 00000000..63b26218
--- /dev/null
+++ b/platypush/backend/http/static/js/dashboard_old.js
@@ -0,0 +1,46 @@
+$(document).ready(function() {
+    var onEvent = function(event) {
+        if (event.args.type == 'platypush.message.event.web.widget.WidgetUpdateEvent') {
+            var $widget = $('#' + event.args.widget);
+            delete event.args.widget;
+
+            for (var key of Object.keys(event.args)) {
+                $widget.find('[data-bind=' + key + ']').text(event.args[key]);
+            }
+        } else if (event.args.type == 'platypush.message.event.web.DashboardIframeUpdateEvent') {
+            var url = event.args.url;
+            var $modal = $('#iframe-modal');
+            var $iframe = $modal.find('iframe');
+            $iframe.attr('src', url);
+            $iframe.prop('width',  event.args.width  || '100%');
+            $iframe.prop('height', event.args.height || '600');
+
+            if ('timeout' in event.args) {
+                setTimeout(function() {
+                    $iframe.removeAttr('src');
+                    $modal.fadeOut();
+                }, parseFloat(event.args.timeout) * 1000);
+            }
+
+            $modal.fadeIn();
+        }
+    };
+
+    var initDashboard = function() {
+        if ('background_image' in window.config) {
+            $('body').css('background-image', 'url(' + window.config.background_image + ')');
+        }
+    };
+
+    var initEvents = function() {
+        window.registerEventListener(onEvent);
+    };
+
+    var init = function() {
+        initDashboard();
+        initEvents();
+    };
+
+    init();
+});
+
diff --git a/platypush/backend/http/static/js/map.js b/platypush/backend/http/static/js/map.js
deleted file mode 100644
index f8cf7dbc..00000000
--- a/platypush/backend/http/static/js/map.js
+++ /dev/null
@@ -1,201 +0,0 @@
-function initMapFromGeopoints(points) {
-    var $mapContainer = $('#map-container'),
-        $map = $('#map');
-
-    var markerArray = [];
-    var directionsService = new google.maps.DirectionsService;
-
-    var map = new google.maps.Map($map.get(0), {
-        mapTypeId: google.maps.MapTypeId.ROADMAP,
-    });
-
-    var bounds = new google.maps.LatLngBounds();
-    var infowindow = new google.maps.InfoWindow();
-    var maxPoints = 22;
-
-    var minDist = 100;
-    var start = points.length > maxPoints ? points.length-maxPoints-1 : 0;
-    var lastPoint;
-
-    for (i = points.length-1; i >= 0 && markerArray.length <= maxPoints; i--) {
-        if (lastPoint && latLngDistance(
-                [points[i]['latitude'], points[i]['longitude']],
-                [lastPoint['latitude'], lastPoint['longitude']]) < minDist) {
-            continue;
-        }
-
-        lastPoint = points[i];
-        var marker = new google.maps.Marker({
-            position: new google.maps.LatLng(points[i]['latitude'], points[i]['longitude']),
-            map: map
-        });
-
-        google.maps.event.addListener(marker, 'click', (function(marker, i) {
-            return function() {
-                infowindow.setContent(
-                    (points[i]['address'] || '[No address]') + ' @ ' +
-                    Date.parse(points[i]['created_at']).toLocaleString());
-                infowindow.open(map, marker);
-            }
-        })(marker, i));
-
-        // Extend the bounds to include each marker's position
-        bounds.extend(marker.position);
-        markerArray.push(marker);
-    }
-
-    var listener = google.maps.event.addListener(map, 'idle', function () {
-        // Now fit the map to the newly inclusive bounds
-        map.fitBounds(bounds);
-        setTimeout(function() {
-            if (window.zoom) {
-                map.setZoom(window.zoom);
-            } else {
-                map.setZoom(getBoundsZoomLevel(bounds, $map.children().width(), $map.children().height()));
-            }
-        }, 1000);
-
-        google.maps.event.removeListener(listener);
-    });
-
-    // Create a renderer for directions and bind it to the map.
-    var directionsDisplay = new google.maps.DirectionsRenderer({map: map});
-
-    // Instantiate an info window to hold step text.
-    var stepDisplay = new google.maps.InfoWindow;
-
-    // Display the route between the initial start and end selections.
-    calculateAndDisplayRoute(
-        directionsDisplay, directionsService, markerArray, stepDisplay, map);
-}
-
-function calculateAndDisplayRoute(directionsDisplay, directionsService,
-    markerArray, stepDisplay, map) {
-    // First, remove any existing markers from the map.
-    for (var i = 0; i < markerArray.length; i++) {
-        markerArray[i].setMap(null);
-    }
-
-    var waypoints = [];
-    for (i=1; i < markerArray.length-1; i++) {
-        if (!waypoints) waypoints = [];
-        waypoints.push({
-            location: markerArray[i].getPosition(),
-            stopover: true,
-        });
-    }
-
-    // Retrieve the start and end locations and create a DirectionsRequest using
-    // WALKING directions.
-    directionsService.route({
-        origin: markerArray[0].getPosition(),
-        destination: markerArray[markerArray.length-1].getPosition(),
-        waypoints: waypoints,
-        travelMode: 'WALKING'
-    }, function(response, status) {
-        // Route the directions and pass the response to a function to create
-        // markers for each step.
-        if (status === 'OK') {
-            directionsDisplay.setDirections(response);
-            showSteps(response, markerArray, stepDisplay, map);
-        } else {
-            // window.alert('Directions request failed due to ' + status);
-        }
-    });
-}
-
-function showSteps(directionResult, markerArray, stepDisplay, map) {
-    // For each step, place a marker, and add the text to the marker's infowindow.
-    // Also attach the marker to an array so we can keep track of it and remove it
-    // when calculating new routes.
-    var myRoute = directionResult.routes[0].legs[0];
-    for (var i = 0; i < myRoute.steps.length; i++) {
-        var marker = markerArray[i] = markerArray[i] || new google.maps.Marker;
-        marker.setMap(map);
-        marker.setPosition(myRoute.steps[i].start_location);
-    }
-}
-
-function getBoundsZoomLevel(bounds, width, height) {
-    var WORLD_DIM = { height: 256, width: 256 };
-    var ZOOM_MAX = 21;
-
-    function latRad(lat) {
-        var sin = Math.sin(lat * Math.PI / 180);
-        var radX2 = Math.log((1 + sin) / (1 - sin)) / 2;
-        return Math.max(Math.min(radX2, Math.PI), -Math.PI) / 2;
-    }
-
-    function zoom(mapPx, worldPx, fraction) {
-        return Math.floor(Math.log(mapPx / worldPx / fraction) / Math.LN2);
-    }
-
-    var ne = bounds.getNorthEast();
-    var sw = bounds.getSouthWest();
-
-    var latFraction = (latRad(ne.lat()) - latRad(sw.lat())) / Math.PI;
-
-    var lngDiff = ne.lng() - sw.lng();
-    var lngFraction = ((lngDiff < 0) ? (lngDiff + 360) : lngDiff) / 360;
-
-    var latZoom = zoom(height, WORLD_DIM.height, latFraction);
-    var lngZoom = zoom(width, WORLD_DIM.width, lngFraction);
-
-    return Math.min(latZoom, lngZoom, ZOOM_MAX);
-}
-
-function latLngDistance(a, b) {
-    if (typeof(Number.prototype.toRad) === "undefined") {
-        Number.prototype.toRad = function() {
-            return this * Math.PI / 180;
-        }
-    }
-
-    var R = 6371e3; // metres
-    var phi1 = a[0].toRad();
-    var phi2 = b[0].toRad();
-    var delta_phi = (b[0]-a[0]).toRad();
-    var delta_lambda = (b[1]-a[1]).toRad();
-
-    var a = Math.sin(delta_phi/2) * Math.sin(delta_phi/2) +
-        Math.cos(phi1) * Math.cos(phi2) *
-        Math.sin(delta_lambda/2) * Math.sin(delta_lambda/2);
-    var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
-
-    return R * c;
-}
-
-function updateGeoPoints() {
-    var from = new Date(window.map_start).toISOString();
-    var to = new Date(window.map_end).toISOString();
-    from = from.substring(0, 10) + ' ' + from.substring(11, 19)
-    to = to.substring(0, 10) + ' ' + to.substring(11, 19)
-
-    var engine = window.db_conf.engine;
-    var table = window.db_conf.table;
-    var columns = window.db_conf.columns;
-
-    execute(
-        {
-            type: 'request',
-            action: 'db.select',
-            args: {
-                engine: engine,
-                query: "SELECT " + columns['latitude'] + " AS latitude, " +
-                    columns['longitude'] + " AS longitude, " +
-                    columns['address'] + " AS address, " +
-                    "DATE_FORMAT(" + columns['created_at'] + ", '%Y-%m-%dT%TZ') " +
-                    "AS created_at FROM " + table + " WHERE created_at BETWEEN '" +
-                    from + "' AND '" + to + "' ORDER BY " + columns['created_at'] + " DESC" }
-        },
-
-        onSuccess = function(response) {
-            initMapFromGeopoints(response.response.output);
-        }
-    );
-}
-
-function initMap() {
-    updateGeoPoints();
-}
-
diff --git a/platypush/backend/http/static/js/plugins/music.mpd/index.js b/platypush/backend/http/static/js/plugins/music.mpd/index.js
index c21e346a..eb930c60 100644
--- a/platypush/backend/http/static/js/plugins/music.mpd/index.js
+++ b/platypush/backend/http/static/js/plugins/music.mpd/index.js
@@ -45,8 +45,6 @@ Vue.component('music-mpd', {
                 timestamp: null,
                 elapsed: null,
             },
-
-            newTrackLock: false,
         };
     },
 
@@ -467,9 +465,6 @@ Vue.component('music-mpd', {
                     }
                 }
             };
-
-            // adjust(this)();
-            // setInterval(adjust(this), 2000);
         },
 
         _parseStatus: async function(status) {
diff --git a/platypush/backend/http/static/js/widgets/calendar.js b/platypush/backend/http/static/js/widgets/calendar.js
deleted file mode 100644
index 566ef9c7..00000000
--- a/platypush/backend/http/static/js/widgets/calendar.js
+++ /dev/null
@@ -1,78 +0,0 @@
-$(document).ready(function() {
-    var $widget = $('.widget.calendar'),
-        $nextEventContainer = $widget.find('.calendar-next-event-container'),
-        $eventsListContainer = $widget.find('.calendar-events-list-container');
-
-    var formatDateString = function(date) {
-        return date.toDateString().substring(0, 10);
-    };
-
-    var formatTimeString = function(date) {
-        return date.toTimeString().substring(0, 5);
-    };
-
-    var refreshStatus = function(status) {
-        setState(state=status.state);
-        if ('elapsed' in status) {
-            setTrackElapsed(status.elapsed);
-        }
-    };
-
-    var refreshCalendar = function() {
-        execute(
-            {
-                type: 'request',
-                action: 'calendar.get_upcoming_events',
-                args: {
-                    max_results: 9,
-                }
-            },
-
-            onSuccess = function(response) {
-                var events = response.response.output;
-                $eventsListContainer.html('');
-
-                for (var i=0; i < events.length; i++) {
-                    var event = events[i];
-                    var start = new Date('dateTime' in event.start ? event.start.dateTime : event.start.date);
-                    var end = new Date('dateTime' in event.end ? event.end.dateTime : event.end.date);
-                    var summary = event.summary;
-
-                    if (i == 0) {
-                        $nextEventContainer.find('.summary').text(summary);
-                        $nextEventContainer.find('.date').text(formatDateString(start));
-                        $nextEventContainer.find('.time').text(
-                            formatTimeString(start) + ' - ' + formatTimeString(end));
-                    } else {
-                        var $event = $('<div></div>').addClass('calendar-event').addClass('row');
-                        var $eventDate = $('<div></div>').addClass('date')
-                            .addClass('three columns').text(formatDateString(start));
-
-                        var $eventTime = $('<div></div>').addClass('time').addClass('one column')
-                            .text('dateTime' in event.start ? formatTimeString(start) : '-');
-
-                        var $eventSummary = $('<div></div>').addClass('summary')
-                            .addClass('eight columns').text(summary);
-
-                        $eventDate.appendTo($event);
-                        $eventTime.appendTo($event);
-                        $eventSummary.appendTo($event);
-                        $event.appendTo($eventsListContainer);
-                    }
-                }
-            }
-        );
-    };
-
-    var initWidget = function() {
-        refreshCalendar();
-        setInterval(refreshCalendar, 900000);
-    };
-
-    var init = function() {
-        initWidget();
-    };
-
-    init();
-});
-
diff --git a/platypush/backend/http/static/js/widgets/calendar/index.js b/platypush/backend/http/static/js/widgets/calendar/index.js
new file mode 100644
index 00000000..c5a299b0
--- /dev/null
+++ b/platypush/backend/http/static/js/widgets/calendar/index.js
@@ -0,0 +1,37 @@
+Vue.component('calendar', {
+    template: '#tmpl-widget-calendar',
+    props: ['config'],
+
+    data: function() {
+        return {
+            events: [],
+        };
+    },
+
+    methods: {
+        refresh: async function() {
+            this.events = (await request('calendar.get_upcoming_events')).map(event => {
+                if (event.start)
+                    event.start = new Date(event.start.dateTime || event.start.date);
+                if (event.end)
+                    event.end = new Date(event.end.dateTime || event.end.date);
+
+                return event;
+            });
+        },
+
+        formatDate: function(date) {
+            return date.toDateString().substring(0, 10);
+        },
+
+        formatTime: function(date) {
+            return date.toTimeString().substring(0, 5);
+        },
+    },
+
+    mounted: function() {
+        this.refresh();
+        setInterval(this.refresh, 600000);
+    },
+});
+
diff --git a/platypush/backend/http/static/js/widgets/date-time-weather.js b/platypush/backend/http/static/js/widgets/date-time-weather.js
deleted file mode 100644
index fba1b722..00000000
--- a/platypush/backend/http/static/js/widgets/date-time-weather.js
+++ /dev/null
@@ -1,108 +0,0 @@
-$(document).ready(function() {
-    var $widget = $('.widget.date-time-weather'),
-        $dateElement = $widget.find('[data-bind=date]'),
-        $timeElement = $widget.find('[data-bind=time]'),
-        $sensorTempElement = $widget.find('[data-bind=sensor-temperature]'),
-        $sensorHumidityElement = $widget.find('[data-bind=sensor-humidity]'),
-        $forecastElement = $widget.find('[data-bind=forecast]'),
-        $tempElement = $widget.find('[data-bind=temperature]'),
-        currentIcon = undefined;
-
-    var onEvent = function(event) {
-        if (event.args.type == 'platypush.message.event.weather.NewWeatherConditionEvent') {
-            updateTemperature(event.args.temperature);
-            updateWeatherIcon(event.args.icon);
-        } else if (event.args.type == 'platypush.message.event.sensor.SensorDataChangeEvent') {
-            if ('temperature' in event.args.data) {
-                updateSensorTemperature(event.args.data.temperature);
-            }
-
-            if ('humidity' in event.args.data) {
-                updateSensorHumidity(event.args.data.humidity);
-            }
-        }
-    };
-
-    var updateTemperature = function(temperature) {
-        $tempElement.text(Math.round(temperature));
-    };
-
-    var updateWeatherIcon = function(icon) {
-        var skycons = new Skycons({
-            'color':'#333', 'resizeClear':'true'
-        });
-
-        if (currentIcon) {
-            skycons.remove('weather-icon');
-        }
-
-        skycons.add('weather-icon', icon);
-        currentIcon = icon;
-    };
-
-    var updateSensorTemperature = function(temperature) {
-        $sensorTempElement.text(Math.round(temperature*10)/10);
-        $sensorTempElement.parent().show();
-    };
-
-    var updateSensorHumidity = function(humidity) {
-        $sensorHumidityElement.text(Math.round(humidity));
-        $sensorHumidityElement.parent().show();
-    };
-
-    var initEvents = function() {
-        window.registerEventListener(onEvent);
-    };
-
-    var refreshDateTime = function() {
-        var now = new Date();
-        $dateElement.text(now.toDateString());
-        $timeElement.text(now.getHours() + ':' +
-            (now.getMinutes() < 10 ? '0' : '') + now.getMinutes() + ':' +
-            (now.getSeconds() < 10 ? '0' : '') + now.getSeconds());
-    };
-
-
-    var initWeather = function() {
-        execute(
-            {
-                type: 'request',
-                action: 'weather.forecast.get_current_weather',
-            },
-
-            onSuccess = function(response) {
-                updateTemperature(status=response.response.output.temperature);
-                updateWeatherIcon(response.response.output.icon);
-            }
-        );
-    };
-
-    var refreshForecast = function() {
-        execute(
-            {
-                type: 'request',
-                action: 'weather.forecast.get_hourly_forecast',
-            },
-
-            onSuccess = function(response) {
-                $forecastElement.text(response.response.output.summary);
-            }
-        );
-    };
-
-    var initWidget = function() {
-        refreshDateTime();
-        setInterval(refreshDateTime, 500);
-        refreshForecast();
-        setInterval(refreshForecast, 1200000);
-        initWeather();
-    };
-
-    var init = function() {
-        initEvents();
-        initWidget();
-    };
-
-    init();
-});
-
diff --git a/platypush/backend/http/static/js/widgets/date-time-weather/index.js b/platypush/backend/http/static/js/widgets/date-time-weather/index.js
new file mode 100644
index 00000000..8b9e0c8b
--- /dev/null
+++ b/platypush/backend/http/static/js/widgets/date-time-weather/index.js
@@ -0,0 +1,68 @@
+Vue.component('date-time-weather', {
+    template: '#tmpl-widget-date-time-weather',
+    props: ['config'],
+
+    data: function() {
+        return {
+            weather: undefined,
+            sensors: {},
+            now: new Date(),
+            weatherIcon: undefined,
+        };
+    },
+
+    methods: {
+        refresh: async function() {
+            let weather = (await request('weather.forecast.get_hourly_forecast')).data[0];
+            this.onWeatherChange(weather);
+        },
+
+        refreshTime: function() {
+            this.now = new Date();
+        },
+
+        formatDate: function(date) {
+            return date.toDateString().substring(0, 10);
+        },
+
+        formatTime: function(date) {
+            return date.toTimeString().substring(0, 8);
+        },
+
+        onWeatherChange: function(event) {
+            if (!this.weather)
+                this.weather = {};
+
+            Vue.set(this, 'weather', {...this.weather, ...event});
+
+            var skycons = new Skycons({
+                'color':'#333', 'resizeClear':'true'
+            });
+
+            if (this.weatherIcon) {
+                skycons.remove('weather-icon');
+            }
+
+            skycons.add('weather-icon', this.weather.icon);
+            this.weatherIcon = this.weather.icon;
+        },
+
+        onSensorData: function(event) {
+            if ('temperature' in event.data)
+                this.sensors.temperature = event.data.temperature;
+
+            if ('humidity' in event.data)
+                this.sensors.temperature = event.data.humidity;
+        },
+    },
+
+    mounted: function() {
+        this.refresh();
+        setInterval(this.refresh, 900000);
+        setInterval(this.refreshTime, 1000);
+
+        registerEventHandler(this.onWeatherChange, 'platypush.message.event.weather.NewWeatherConditionEvent');
+        registerEventHandler(this.onSensorData, 'platypush.message.event.sensor.SensorDataChangeEvent');
+    },
+});
+
diff --git a/platypush/backend/http/static/js/widgets/skycons.js b/platypush/backend/http/static/js/widgets/date-time-weather/skycons.js
similarity index 100%
rename from platypush/backend/http/static/js/widgets/skycons.js
rename to platypush/backend/http/static/js/widgets/date-time-weather/skycons.js
diff --git a/platypush/backend/http/static/js/widgets/image-carousel.js b/platypush/backend/http/static/js/widgets/image-carousel.js
deleted file mode 100644
index de95196e..00000000
--- a/platypush/backend/http/static/js/widgets/image-carousel.js
+++ /dev/null
@@ -1,57 +0,0 @@
-$(document).ready(function() {
-    var $imgContainer = $('.image-carousel').find('.carousel'),
-        $imgBackground = $('.image-carousel').find('.carousel-background'),
-        config = window.widgets['image-carousel'],
-        images = config.imageUrls,
-        processedImages = 0;
-
-    var shuffleImages = function() {
-		for (var i=images.length-1; i > 0; i--) {
-			var j = Math.floor(Math.random() * (i + 1));
-			var x = images[i];
-			images[i] = images[j];
-			images[j] = x;
-        }
-    };
-
-    var refreshImage = function() {
-        var nextImage = images[processedImages++];
-        var $oldImg = $imgContainer.find('img');
-        var $newImg = $('<img></img>')
-            .attr('src', nextImage)
-            .attr('alt', 'Could not load image')
-            .appendTo('body').hide();
-
-        $newImg.on('load', function() {
-            $oldImg.remove();
-            if ($newImg.width() > $newImg.height()) {
-                $newImg.css('width', '100%');
-                $imgBackground.css('background-image', '');
-            } else {
-                $imgBackground.css('background-image', 'url(' + nextImage + ')');
-            }
-
-            $newImg.css('max-height', '100%');
-            $newImg.remove().appendTo($imgContainer).show();
-        });
-
-        if (processedImages == images.length-1) {
-            shuffleImages();
-            processedImages = 0;
-        }
-    };
-
-    var initWidget = function() {
-        shuffleImages();
-        refreshImage();
-        setInterval(refreshImage,
-            'refresh_seconds' in config ? config.refresh_seconds * 1000 : 15000);
-    };
-
-    var init = function() {
-        initWidget();
-    };
-
-    init();
-});
-
diff --git a/platypush/backend/http/static/js/widgets/image-carousel/index.js b/platypush/backend/http/static/js/widgets/image-carousel/index.js
new file mode 100644
index 00000000..b5d5090e
--- /dev/null
+++ b/platypush/backend/http/static/js/widgets/image-carousel/index.js
@@ -0,0 +1,60 @@
+Vue.component('image-carousel', {
+    template: '#tmpl-widget-image-carousel',
+    props: ['config'],
+
+    data: function() {
+        return {
+            images: [],
+            currentImage: undefined,
+        };
+    },
+
+    methods: {
+        refresh: async function() {
+            if (!this.images.length) {
+                this.images = await request('utils.search_web_directory', {
+                    directory: this.config.images_path,
+                    extensions: ['.jpg', '.jpeg', '.png'],
+                });
+
+                this.shuffleImages();
+            }
+
+            this.currentImage = this.images.pop();
+        },
+
+        onNewImage: function() {
+            this.$refs.background.style['background-image'] = 'url(' + this.currentImage + ')';
+
+            if (this.$refs.img.width > this.$refs.img.height) {
+                this.$refs.img.style.width = 'auto';
+
+                if ((this.$refs.img.width / this.$refs.img.height) >= 4/3) {
+                    this.$refs.img.style.width = '100%';
+                }
+
+                if ((this.$refs.img.width / this.$refs.img.height) <= 16/9) {
+                    this.$refs.img.style.height = '100%';
+                }
+            }
+        },
+
+        shuffleImages: function() {
+            for (var i=this.images.length-1; i > 0; i--) {
+                let j = Math.floor(Math.random() * (i+1));
+                let x = this.images[i];
+                Vue.set(this.images, i, this.images[j]);
+                Vue.set(this.images, j, x);
+            }
+        },
+    },
+
+    mounted: function() {
+        this.$refs.img.addEventListener('load', this.onNewImage);
+        this.$refs.img.addEventListener('error', this.refresh);
+
+        this.refresh();
+        setInterval(this.refresh, 'refresh_seconds' in this.config ? this.config.refresh_seconds*1000 : 15000);
+    },
+});
+
diff --git a/platypush/backend/http/static/js/widgets/music.js b/platypush/backend/http/static/js/widgets/music.js
deleted file mode 100644
index 4d73dcdf..00000000
--- a/platypush/backend/http/static/js/widgets/music.js
+++ /dev/null
@@ -1,192 +0,0 @@
-$(document).ready(function() {
-    var $widget = $('.widget.music'),
-        $trackContainer = $widget.find('.track-container'),
-        $timeContainer = $widget.find('.time-container'),
-        $playbackStatusContainer = $widget.find('.playback-status-container'),
-        $noTrackElement = $trackContainer.find('.no-track-info'),
-        $trackElement = $trackContainer.find('.track-info'),
-        $artistElement = $trackElement.find('[data-bind=artist]'),
-        $titleElement = $trackElement.find('[data-bind=title]'),
-        $timeElapsedElement = $timeContainer.find('.time-elapsed'),
-        $timeTotalElement = $timeContainer.find('.time-total'),
-        $elapsedTimeBar = $widget.find('.time-bar > .elapsed'),
-        $volumeElement = $playbackStatusContainer.find('[data-bind=playback-volume]'),
-        $randomElement = $playbackStatusContainer.find('[data-bind=playback-random]'),
-        $repeatElement = $playbackStatusContainer.find('[data-bind=playback-repeat]'),
-        $singleElement = $playbackStatusContainer.find('[data-bind=playback-single]'),
-        $consumeElement = $playbackStatusContainer.find('[data-bind=playback-consume]'),
-        timeElapsed,
-        timeTotal,
-        refreshElapsedInterval;
-
-    var onEvent = function(event) {
-        switch (event.args.type) {
-            case 'platypush.message.event.music.NewPlayingTrackEvent':
-                createNotification({
-                    'icon': 'play',
-                    'html': '<b>' + ('artist' in event.args.track ? event.args.track.artist : '')
-                                  + '</b><br/>'
-                                  + ('title' in event.args.track ? event.args.track.title : '[No name]'),
-                });
-
-            case 'platypush.message.event.music.MusicPlayEvent':
-            case 'platypush.message.event.music.MusicPauseEvent':
-                refreshTrack(event.args.track);
-
-            case 'platypush.message.event.music.MusicStopEvent':
-                refreshStatus(event.args.status);
-                break;
-
-            case 'platypush.message.event.music.VolumeChangeEvent':
-            case 'platypush.message.event.music.PlaybackRepeatModeChangeEvent':
-            case 'platypush.message.event.music.PlaybackRandomModeChangeEvent':
-            case 'platypush.message.event.music.PlaybackConsumeModeChangeEvent':
-            case 'platypush.message.event.music.PlaybackSingleModeChangeEvent':
-                refreshPlaybackStatus(event.args.status);
-                break;
-        }
-    };
-
-    var initEvents = function() {
-        window.registerEventListener(onEvent);
-    };
-
-
-    var setState = function(state) {
-        if (state === 'play') {
-            $noTrackElement.hide();
-            $trackElement.show();
-            $timeContainer.show();
-        } else if (state === 'pause') {
-            $noTrackElement.hide();
-            $trackElement.show();
-            $timeContainer.hide();
-        } else if (state === 'stop') {
-            $noTrackElement.show();
-            $trackElement.hide();
-            $timeContainer.hide();
-        }
-    };
-
-    var secondsToTimeString = function(seconds) {
-        seconds = parseInt(seconds);
-
-        if (seconds) {
-            return (parseInt(seconds/60) + ':' +
-                (seconds%60 < 10 ? '0' : '') + seconds%60);
-        } else {
-            return '-:--';
-        }
-    };
-
-    var setTrackTime = function(time) {
-        $timeTotalElement.text(secondsToTimeString(time));
-        timeTotal = parseInt(time);
-    };
-
-    var setTrackElapsed = function(time) {
-        if (refreshElapsedInterval) {
-            clearInterval(refreshElapsedInterval);
-            refreshElapsedInterval = undefined;
-        }
-
-        if (time === undefined) {
-            $timeElapsedElement.text('-:--');
-            return;
-        }
-
-        timeElapsed = parseInt(time);
-        $timeElapsedElement.text(secondsToTimeString(timeElapsed));
-
-        var ratio = 100 * Math.min(timeElapsed/timeTotal, 1);
-        $elapsedTimeBar.css('width', ratio + '%');
-
-        refreshElapsedInterval = setInterval(function() {
-            timeElapsed += 1;
-            ratio = 100 * Math.min(timeElapsed/timeTotal, 1);
-            $elapsedTimeBar.css('width', ratio + '%');
-            $timeElapsedElement.text(secondsToTimeString(timeElapsed));
-        }, 1000);
-    };
-
-    var refreshStatus = function(status) {
-        if (!status) {
-            return;
-        }
-
-        if ('state' in status) {
-            setState(state=status.state);
-            if (status.state === 'stop') {
-                setTrackElapsed();
-            }
-        }
-
-        if ('elapsed' in status) {
-            setTrackElapsed(status.elapsed);
-        } else if ('position' in status) {
-            setTrackElapsed(status.position);
-        }
-    };
-
-    var refreshTrack = function(track) {
-        if (!track) {
-            return;
-        }
-
-        if ('time' in track) {
-            setTrackTime(track.time);
-        }
-
-        $artistElement.text(track.artist);
-        $titleElement.text(track.title);
-    };
-
-    var refreshPlaybackStatus = function(status) {
-        if (!status) {
-            return;
-        }
-
-        if ('volume' in status) {
-            $volumeElement.text(status.volume + '%');
-        }
-
-        if ('random' in status) {
-            var state = !!parseInt(status.random);
-            $randomElement.text(state ? 'ON' : 'OFF');
-        }
-
-        if ('repeat' in status) {
-            var state = !!parseInt(status.repeat);
-            $repeatElement.text(state ? 'ON' : 'OFF');
-        }
-
-        if ('single' in status) {
-            var state = !!parseInt(status.single);
-            $singleElement.text(state ? 'ON' : 'OFF');
-        }
-
-        if ('consume' in status) {
-            var state = !!parseInt(status.consume);
-            $consumeElement.text(state ? 'ON' : 'OFF');
-        }
-    };
-
-    var initWidget = function() {
-        $.when(
-            execute({ type: 'request', action: 'music.mpd.currentsong' }),
-            execute({ type: 'request', action: 'music.mpd.status' })
-        ).done(function(t, s) {
-            refreshTrack(t[0].response.output);
-            refreshStatus(s[0].response.output);
-            refreshPlaybackStatus(s[0].response.output);
-        });
-    };
-
-    var init = function() {
-        initEvents();
-        initWidget();
-    };
-
-    init();
-});
-
diff --git a/platypush/backend/http/static/js/widgets/music/index.js b/platypush/backend/http/static/js/widgets/music/index.js
new file mode 100644
index 00000000..0626f101
--- /dev/null
+++ b/platypush/backend/http/static/js/widgets/music/index.js
@@ -0,0 +1,233 @@
+Vue.component('music', {
+    template: '#tmpl-widget-music',
+    props: ['config'],
+
+    data: function() {
+        return {
+            track: undefined,
+            status: undefined,
+            timer: undefined,
+
+            syncTime: {
+                timestamp: null,
+                elapsed: null,
+            },
+        };
+    },
+
+    methods: {
+        refresh: async function() {
+            let status = await request('music.mpd.status');
+            let track = await request('music.mpd.currentsong');
+
+            this._parseStatus(status);
+            this._parseTrack(track);
+
+            if (status.state === 'play' && !this.timer)
+                this.startTimer();
+            else if (status.state !== 'play' && this.timer)
+                this.stopTimer();
+        },
+
+        convertTime: function(time) {
+            time = parseFloat(time);   // Normalize strings
+            var t = {};
+            t.h = '' + parseInt(time/3600);
+            t.m = '' + parseInt(time/60 - t.h*60);
+            t.s = '' + parseInt(time - (t.h*3600 + t.m*60));
+
+            for (var attr of ['m','s']) {
+                if (parseInt(t[attr]) < 10) {
+                    t[attr] = '0' + t[attr];
+                }
+            }
+
+            var ret = [];
+            if (parseInt(t.h)) {
+                ret.push(t.h);
+            }
+
+            ret.push(t.m, t.s);
+            return ret.join(':');
+        },
+
+        _parseStatus: async function(status) {
+            if (!status || status.length === 0)
+                status = await request('music.mpd.status');
+
+            if (!this.status)
+                this.status = {};
+
+            for (const [attr, value] of Object.entries(status)) {
+                if (['consume','random','repeat','single','bitrate'].indexOf(attr) >= 0) {
+                    Vue.set(this.status, attr, !!parseInt(value));
+                } else if (['nextsong','nextsongid','playlist','playlistlength',
+                            'volume','xfade','song','songid'].indexOf(attr) >= 0) {
+                    Vue.set(this.status, attr, parseInt(value));
+                } else if (['elapsed'].indexOf(attr) >= 0) {
+                    Vue.set(this.status, attr, parseFloat(value));
+                } else {
+                    Vue.set(this.status, attr, value);
+                }
+            }
+        },
+
+        _parseTrack: async function(track) {
+            if (!track || track.length === 0) {
+                track = await request('music.mpd.currentsong');
+            }
+
+            if (!this.track)
+                this.track = {};
+
+            for (const [attr, value] of Object.entries(track)) {
+                if (['id','pos','time','track','disc'].indexOf(attr) >= 0) {
+                    Vue.set(this.track, attr, parseInt(value));
+                } else {
+                    Vue.set(this.track, attr, value);
+                }
+            }
+        },
+
+        showNewTrackNotification: function() {
+            createNotification({
+                html: '<b>' + (this.track.artist || '[No Artist]') + '</b><br>' +
+                      (this.track.title || '[No Title]'),
+                image: {
+                    icon: 'play',
+                }
+            });
+        },
+
+        onNewPlayingTrack: async function(event) {
+            let previousTrack = undefined;
+
+            if (this.track) {
+                previousTrack = {
+                    file: this.track.file,
+                    artist: this.track.artist,
+                    title: this.track.title,
+                };
+            }
+
+            this.status.state = 'play';
+            Vue.set(this.status, 'elapsed', 0);
+            this.track = {};
+            this._parseTrack(event.track);
+
+            let status = event.status ? event.status : await request('music.mpd.status');
+            this._parseStatus(status);
+            this.startTimer();
+
+            if (previousTrack && this.track.file != previousTrack.file
+                    || this.track.artist != previousTrack.artist
+                    || this.track.title != previousTrack.title) {
+                this.showNewTrackNotification();
+            }
+        },
+
+        onMusicStop: function(event) {
+            this.status.state = 'stop';
+            Vue.set(this.status, 'elapsed', 0);
+            this._parseStatus(event.status);
+            this._parseTrack(event.track);
+            this.stopTimer();
+        },
+
+        onMusicPlay: function(event) {
+            this.status.state = 'play';
+            this._parseStatus(event.status);
+            this._parseTrack(event.track);
+            this.startTimer();
+        },
+
+        onMusicPause: function(event) {
+            this.status.state = 'pause';
+            this._parseStatus(event.status);
+            this._parseTrack(event.track);
+
+            Vue.set(this.syncTime, 'timestamp', new Date());
+            Vue.set(this.syncTime, 'elapsed', this.status.elapsed);
+        },
+
+        onSeekChange: function(event) {
+            if (event.position != null)
+                Vue.set(this.status, 'elapsed', parseFloat(event.position));
+            if (event.status)
+                this._parseStatus(event.status);
+            if (event.track)
+                this._parseTrack(event.track);
+
+            Vue.set(this.syncTime, 'timestamp', new Date());
+            Vue.set(this.syncTime, 'elapsed', this.status.elapsed);
+        },
+
+        onVolumeChange: function(event) {
+            if (event.volume != null)
+                this.status.volume = parseFloat(event.volume);
+            if (event.status)
+                this._parseStatus(event.status);
+            if (event.track)
+                this._parseTrack(event.track);
+        },
+
+        onRepeatChange: function(event) {
+            this.status.repeat = event.state;
+        },
+
+        onRandomChange: function(event) {
+            this.status.random = event.state;
+        },
+
+        onConsumeChange: function(event) {
+            this.status.consume = event.state;
+        },
+
+        onSingleChange: function(event) {
+            this.status.single = event.state;
+        },
+
+        startTimer: function() {
+            if (this.timer != null) {
+                this.stopTimer();
+            }
+
+            Vue.set(this.syncTime, 'timestamp', new Date());
+            Vue.set(this.syncTime, 'elapsed', this.status.elapsed);
+            this.timer = setInterval(this.timerFunc, 1000);
+        },
+
+        stopTimer: function() {
+            if (this.timer == null) {
+                clearInterval(this.timer);
+                this.timer = null;
+            }
+        },
+
+        timerFunc: function() {
+            if (this.status.state !== 'play' || this.status.elapsed == null) {
+                return;
+            }
+
+            Vue.set(this.status, 'elapsed', this.syncTime.elapsed +
+                ((new Date()).getTime()/1000) - (this.syncTime.timestamp.getTime()/1000));
+        },
+    },
+
+    mounted: function() {
+        this.refresh();
+        setInterval(this.refresh, 60000);
+
+        registerEventHandler(this.onNewPlayingTrack, 'platypush.message.event.music.NewPlayingTrackEvent');
+        registerEventHandler(this.onMusicStop, 'platypush.message.event.music.MusicStopEvent');
+        registerEventHandler(this.onMusicPlay, 'platypush.message.event.music.MusicPlayEvent');
+        registerEventHandler(this.onMusicPause, 'platypush.message.event.music.MusicPauseEvent');
+        registerEventHandler(this.onSeekChange, 'platypush.message.event.music.SeekChangeEvent');
+        registerEventHandler(this.onVolumeChange, 'platypush.message.event.music.VolumeChangeEvent');
+        registerEventHandler(this.onRepeatChange, 'platypush.message.event.music.PlaybackRepeatModeChangeEvent');
+        registerEventHandler(this.onRandomChange, 'platypush.message.event.music.PlaybackRandomModeChangeEvent');
+        registerEventHandler(this.onConsumeChange, 'platypush.message.event.music.PlaybackConsumeModeChangeEvent');
+        registerEventHandler(this.onSingleChange, 'platypush.message.event.music.PlaybackSingleModeChangeEvent');
+    },
+});
+
diff --git a/platypush/backend/http/static/js/widgets/rss-news.js b/platypush/backend/http/static/js/widgets/rss-news.js
deleted file mode 100644
index 576d5222..00000000
--- a/platypush/backend/http/static/js/widgets/rss-news.js
+++ /dev/null
@@ -1,80 +0,0 @@
-$(document).ready(function() {
-    var $newsElement = $('.rss-news').find('.news-container'),
-        config = window.widgets['rss-news'],
-        db = config.db,
-        news = [],
-        default_limit = 10,
-        cur_article_index = -1;
-
-    var getNews = function() {
-        execute(
-            {
-                type: 'request',
-                action: 'db.select',
-                args: {
-                    engine: db,
-                    query: "select s.title as source, e.title, e.summary, " +
-                        "strftime('%Y-%m-%dT%H:%M:%fZ', e.published) as published " +
-                        "from FeedEntry e join FeedSource s " +
-                        "on e.source_id = s.id order by e.published desc limit " +
-                        ('limit' in config ? config.limit : default_limit)
-                }
-            },
-
-            onSuccess = function(response) {
-                if (!response.response.output) {
-                    return;
-                }
-
-                var firstRun = news.length === 0;
-                news = response.response.output;
-                cur_article_index = -1;
-
-                if (firstRun) {
-                    refreshNews();
-                }
-            }
-        );
-    };
-
-    var refreshNews = function() {
-        if (news.length === 0) {
-            return;
-        }
-
-        var updateNewsList = cur_article_index == news.length-1;
-        var nextArticle = news[++cur_article_index % news.length];
-        var dt = new Date(nextArticle.published);
-        var $article = $('<div></div>').addClass('article');
-        var $source = $('<div></div>').addClass('source').text(nextArticle.source);
-        var $title = $('<div></div>').addClass('title').text(nextArticle.title);
-        var $publishTime = $('<div></div>').addClass('publish-time')
-            .text(dt.toDateString() + ', ' + dt.toTimeString().substring(0, 5));
-
-        $source.appendTo($article);
-        $title.appendTo($article);
-        $publishTime.appendTo($article);
-
-        if ($newsElement.find('.article').length) {
-            $newsElement.find('.article').remove();
-        }
-
-        $article.hide().appendTo($newsElement).show();
-
-        if (updateNewsList) {
-            getNews();
-        }
-    };
-
-    var initWidget = function() {
-        getNews();
-        setInterval(refreshNews, 15000);
-    };
-
-    var init = function() {
-        initWidget();
-    };
-
-    init();
-});
-
diff --git a/platypush/backend/http/static/js/widgets/rss-news/index.js b/platypush/backend/http/static/js/widgets/rss-news/index.js
new file mode 100644
index 00000000..2f878959
--- /dev/null
+++ b/platypush/backend/http/static/js/widgets/rss-news/index.js
@@ -0,0 +1,40 @@
+Vue.component('rss-news', {
+    template: '#tmpl-widget-rss-news',
+    props: ['config'],
+
+    data: function() {
+        return {
+            articles: [],
+            queue: [],
+            currentArticle: undefined,
+        };
+    },
+
+    methods: {
+        refresh: async function() {
+            if (!this.queue.length) {
+                this.articles = await request('db.select', {
+                    engine: this.config.db,
+                    query: "select s.title as source, e.title, e.summary, " +
+                    "strftime('%Y-%m-%dT%H:%M:%fZ', e.published) as published " +
+                    "from FeedEntry e join FeedSource s " +
+                    "on e.source_id = s.id order by e.published limit " +
+                    ('limit' in this.config ? this.config.limit : 10)
+                });
+
+                this.queue = [...this.articles];
+            }
+
+            if (!this.queue.length)
+                return;
+
+            this.currentArticle = this.queue.pop();
+        },
+    },
+
+    mounted: function() {
+        this.refresh();
+        setInterval(this.refresh, 'refresh_seconds' in this.config ? this.config.refresh_seconds*1000 : 15000);
+    },
+});
+
diff --git a/platypush/backend/http/templates/dashboard.html b/platypush/backend/http/templates/dashboard.html
index 48a8b211..5021c01e 100644
--- a/platypush/backend/http/templates/dashboard.html
+++ b/platypush/backend/http/templates/dashboard.html
@@ -1,72 +1,107 @@
 <!doctype html>
 <head>
     <title>Platypush Dashboard</title>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
 
-    <link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Lato" />
-    <link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton.css') }}"></script>
-    <link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton-tabs.css') }}"></script>
-    <link rel="stylesheet" href="{{ url_for('static', filename='css/normalize.css') }}"></script>
-    <link rel="stylesheet" href="{{ url_for('static', filename='font-awesome/css/font-awesome.min.css') }}"></script>
-    <link rel="stylesheet" href="{{ url_for('static', filename='css/application.css') }}"></script>
-    <link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}"></script>
-    <link rel="stylesheet" href="{{ url_for('static', filename='css/toggles.css') }}"></script>
+    <link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Lato">
+    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/skeleton.css') }}">
+    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/normalize.css') }}">
+    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='font-awesome/css/all.css') }}">
+    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/dist/dashboard.css') }}">
+
+    <script type="text/javascript" src="{{ url_for('static', filename='js/lib/vue.js') }}"></script>
+    <script type="text/javascript" src="{{ url_for('static', filename='js/lib/axios.min.js') }}"></script>
+
+    <script type="text/javascript" src="{{ url_for('static', filename='js/api.js') }}"></script>
+    <script type="text/javascript" src="{{ url_for('static', filename='js/events.js') }}"></script>
+    <script type="text/javascript" src="{{ url_for('static', filename='js/notifications.js') }}"></script>
+
+    <script type="text/javascript" src="{{ url_for('static', filename='js/plugins/pushbullet/index.js') }}"></script>
+    <script type="text/javascript" src="{{ url_for('static', filename='js/plugins/assistant.google/index.js') }}"></script>
 
-    <script type="text/javascript" src="{{ url_for('static', filename='js/jquery-3.3.1.min.js') }}"></script>
-    <script type="text/javascript" src="{{ url_for('static', filename='js/skeleton-tabs.js') }}"></script>
-    <script type="text/javascript" src="{{ url_for('static', filename='js/application.js') }}"></script>
-    <script type="text/javascript" src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
-    <script type="text/javascript" src="{{ url_for('static', filename='js/pushbullet.js') }}"></script>
-    <script type="text/javascript" src="{{ url_for('static', filename='js/assistant.google.js') }}"></script>
     <script type="text/javascript">
-        window.websocket_port = {% print(websocket_port) %};
-        window.has_ssl = {% print('true' if has_ssl else 'false') %};
+        if (!window.config) {
+            window.config = {};
+        }
+
+        window.config = { ...window.config,
+            websocket_port: {{ websocket_port }},
+            has_ssl: {{ 'true' if has_ssl else 'false' }},
+            templates: JSON.parse('{{ utils.to_json(templates)|safe }}'),
+            scripts: JSON.parse('{{ utils.to_json(scripts)|safe }}'),
+        };
 
         {% if token %}
-            window.token = '{% print(token) %}';
+            window.config.token = '{{ token }}';
         {% else %}
-            window.token = undefined;
+            window.config.token = undefined;
         {% endif %}
 
-        window.config = {{ config | safe }};
-        window.widgets = {{ config['widgets'] | safe }}.reduce(function(map, w) { map[w.widget] = w; return map }, {})
+        window.config.dashboard = {{ config | safe }};
+        window.config.widgets = {{ config['widgets'] | safe }}.reduce(function(map, w) { map[w.widget] = w; return map }, {})
     </script>
+
+    {% include 'elements.html' %}
+
+    {% for widget in config['widgets'] %}
+        {% with name = widget['widget'] %}
+            {% include 'widgets/' + name + '/index.html' %}
+
+            {% with js_file = static_folder + '/js/widgets/' + name + '/index.js' %}
+                {% if utils.isfile(js_file) %}
+                    <script type="text/javascript" src="{{ url_for('static', filename='js/widgets/' + name + '/index.js') }}"></script>
+                {% endif %}
+            {% endwith %}
+
+            {% with css_file = static_folder + '/css/dist/dashboard/widgets/' + name + '.css' %}
+                {% if utils.isfile(css_file) %}
+                    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/dist/dashboard/widgets/' + name + '.css') }}">
+                {% endif %}
+            {% endwith %}
+        {% endwith %}
+    {% endfor %}
 </head>
 
 <body>
-    <main>
-        <!-- You can send events of type platypush.message.event.web.DashboardIframeUpdateEvent
-             to control what is shown in the optional iframe modal -->
-        <div id="iframe-modal" class="modal">
-            <div class="modal-container">
-                <iframe></iframe>
-            </div>
-        </div>
+    <div id="app">
+        <main>
+            <!-- You can send events of type platypush.message.event.web.DashboardIframeUpdateEvent
+                 to control what is shown in the optional iframe modal -->
+            <modal id="iframe-modal" v-model="iframeModal.visible">
+                <iframe ref="iframeModal"></iframe>
+            </modal>
 
-        <div id="widgets-container">
-            {% set used_columns = [0] %}
-            {% for widget in config['widgets'] %}
-                {% if used_columns[0] % 12 == 0 %}
-                <div class="row">
-                {% endif %}
+            <div id="widgets-container">
+                {% set used_columns = [0] %}
+                {% for widget in config['widgets'] %}
+                    {% with name = widget['widget'] %}
+                        {% if used_columns[0] % 12 == 0 %}
+                            <div class="widgets-row">
+                        {% endif %}
 
-                <div class="widget {% print(utils.widget_columns_to_html_class(widget['columns'])) %}
-                        {% print(widget['widget']) %}"
-                        id="{% print(widget['id'] if 'id' in widget else widget['widget']) %}">
-                    {% with properties=widget %}
-                        {% include 'widgets/' + widget['widget'] + '.html' %}
+                        {% with properties=widget %}
+                            <widget :tag="'{{ name }}'.replace('.', '-')"
+                                    key="{{ name }}"
+                                    :config="{{ widget | safe }}">
+                            </widget>
+                        {% endwith %}
+
+                        {# increment counter #}
+                        {% if used_columns.append(used_columns.pop() + widget['columns']) %}{% endif %}
+
+                        {% if used_columns[0] % 12 == 0 %}
+                            </div>
+                        {% endif %}
                     {% endwith %}
-                </div>
+                {% endfor %}
+            </div>
 
-                {# increment counter #}
-                {% if used_columns.append(used_columns.pop() + widget['columns']) %}{% endif %}
+            {% include 'notifications.html' %}
+        </main>
+    </div>
 
-                {% if used_columns[0] % 12 == 0 %}
-                </div>
-                {% endif %}
-            {% endfor %}
-        </div>
+    {% include 'widgets/template.html' %}
 
-        <div id="notification-container"></div>
-    </main>
-<body>
+    <script type="text/javascript" src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
+</body>
 
diff --git a/platypush/backend/http/templates/dashboard_old.html b/platypush/backend/http/templates/dashboard_old.html
new file mode 100644
index 00000000..48a8b211
--- /dev/null
+++ b/platypush/backend/http/templates/dashboard_old.html
@@ -0,0 +1,72 @@
+<!doctype html>
+<head>
+    <title>Platypush Dashboard</title>
+
+    <link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Lato" />
+    <link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton.css') }}"></script>
+    <link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton-tabs.css') }}"></script>
+    <link rel="stylesheet" href="{{ url_for('static', filename='css/normalize.css') }}"></script>
+    <link rel="stylesheet" href="{{ url_for('static', filename='font-awesome/css/font-awesome.min.css') }}"></script>
+    <link rel="stylesheet" href="{{ url_for('static', filename='css/application.css') }}"></script>
+    <link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}"></script>
+    <link rel="stylesheet" href="{{ url_for('static', filename='css/toggles.css') }}"></script>
+
+    <script type="text/javascript" src="{{ url_for('static', filename='js/jquery-3.3.1.min.js') }}"></script>
+    <script type="text/javascript" src="{{ url_for('static', filename='js/skeleton-tabs.js') }}"></script>
+    <script type="text/javascript" src="{{ url_for('static', filename='js/application.js') }}"></script>
+    <script type="text/javascript" src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
+    <script type="text/javascript" src="{{ url_for('static', filename='js/pushbullet.js') }}"></script>
+    <script type="text/javascript" src="{{ url_for('static', filename='js/assistant.google.js') }}"></script>
+    <script type="text/javascript">
+        window.websocket_port = {% print(websocket_port) %};
+        window.has_ssl = {% print('true' if has_ssl else 'false') %};
+
+        {% if token %}
+            window.token = '{% print(token) %}';
+        {% else %}
+            window.token = undefined;
+        {% endif %}
+
+        window.config = {{ config | safe }};
+        window.widgets = {{ config['widgets'] | safe }}.reduce(function(map, w) { map[w.widget] = w; return map }, {})
+    </script>
+</head>
+
+<body>
+    <main>
+        <!-- You can send events of type platypush.message.event.web.DashboardIframeUpdateEvent
+             to control what is shown in the optional iframe modal -->
+        <div id="iframe-modal" class="modal">
+            <div class="modal-container">
+                <iframe></iframe>
+            </div>
+        </div>
+
+        <div id="widgets-container">
+            {% set used_columns = [0] %}
+            {% for widget in config['widgets'] %}
+                {% if used_columns[0] % 12 == 0 %}
+                <div class="row">
+                {% endif %}
+
+                <div class="widget {% print(utils.widget_columns_to_html_class(widget['columns'])) %}
+                        {% print(widget['widget']) %}"
+                        id="{% print(widget['id'] if 'id' in widget else widget['widget']) %}">
+                    {% with properties=widget %}
+                        {% include 'widgets/' + widget['widget'] + '.html' %}
+                    {% endwith %}
+                </div>
+
+                {# increment counter #}
+                {% if used_columns.append(used_columns.pop() + widget['columns']) %}{% endif %}
+
+                {% if used_columns[0] % 12 == 0 %}
+                </div>
+                {% endif %}
+            {% endfor %}
+        </div>
+
+        <div id="notification-container"></div>
+    </main>
+<body>
+
diff --git a/platypush/backend/http/templates/index.html b/platypush/backend/http/templates/index.html
index c6def109..e00fbb85 100644
--- a/platypush/backend/http/templates/index.html
+++ b/platypush/backend/http/templates/index.html
@@ -68,7 +68,8 @@
                         :tag="plugin.replace('.', '-')"
                         :key="plugin"
                         :config="conf"
-                        :class="{hidden: plugin != selectedPlugin}"/>
+                        :class="{hidden: plugin != selectedPlugin}">
+                </plugin>
             </div>
         </main>
 
diff --git a/platypush/backend/http/templates/plugins/assistant.google.html b/platypush/backend/http/templates/plugins/assistant.google.html
deleted file mode 100644
index a6e62c1d..00000000
--- a/platypush/backend/http/templates/plugins/assistant.google.html
+++ /dev/null
@@ -1,3 +0,0 @@
-<script type="text/javascript" src="{{ url_for('static', filename='js/assistant.google.js') }}"></script>
-<link rel="stylesheet" href="{{ url_for('static', filename='css/assistant.google.css') }}"></script>
-
diff --git a/platypush/backend/http/templates/plugins/gpio.html b/platypush/backend/http/templates/plugins/gpio.html
deleted file mode 100644
index ac652297..00000000
--- a/platypush/backend/http/templates/plugins/gpio.html
+++ /dev/null
@@ -1,5 +0,0 @@
-<script type="text/javascript" src="{{ url_for('static', filename='js/gpio.js') }}"></script>
-<link rel="stylesheet" href="{{ url_for('static', filename='css/gpio.css') }}"></script>
-
-<div class="row" id="gpio-container"></div>
-
diff --git a/platypush/backend/http/templates/plugins/gpio.sensor.mcp3008.html b/platypush/backend/http/templates/plugins/gpio.sensor.mcp3008.html
deleted file mode 100644
index be1a5151..00000000
--- a/platypush/backend/http/templates/plugins/gpio.sensor.mcp3008.html
+++ /dev/null
@@ -1,5 +0,0 @@
-<script type="text/javascript" src="{{ url_for('static', filename='js/gpio.sensor.mcp3008.js') }}"></script>
-<link rel="stylesheet" href="{{ url_for('static', filename='css/gpio.sensor.mcp3008.css') }}"></script>
-
-<div class="row" id="sensors-container"></div>
-
diff --git a/platypush/backend/http/templates/plugins/gpio.zeroborg.html b/platypush/backend/http/templates/plugins/gpio.zeroborg.html
deleted file mode 100644
index 8ef6e3db..00000000
--- a/platypush/backend/http/templates/plugins/gpio.zeroborg.html
+++ /dev/null
@@ -1,44 +0,0 @@
-<script type="text/javascript" src="{{ url_for('static', filename='js/gpio.zeroborg.js') }}"></script>
-<link rel="stylesheet" href="{{ url_for('static', filename='css/gpio.zeroborg.css') }}"></script>
-
-<div id="zb-container" class="row" tabindex="1">
-    <div class="zb-controls-container">
-        <div class="row">
-            <div class="four columns">&nbsp;</div>
-            <button data-direction="up" class="zb-ctrl-btn four columns">
-                <i class="fa fa-sort-up"></i>
-            </button>
-            <div class="four columns">&nbsp;</div>
-        </div>
-
-        <div class="row">
-            <button data-direction="left" class="zb-ctrl-btn four columns">
-                <i class="fa fa-caret-left"></i>
-            </button>
-            <div class="four columns">&nbsp;</div>
-            <button data-direction="right" class="zb-ctrl-btn four columns">
-                <i class="fa fa-caret-right"></i>
-            </button>
-        </div>
-
-        <div class="row">
-            <div class="four columns">&nbsp;</div>
-            <button data-direction="down" class="zb-ctrl-btn four columns">
-                <i class="fa fa-sort-down"></i>
-            </button>
-            <div class="four columns">&nbsp;</div>
-        </div>
-
-        <div class="row ctrl-bottom-row">
-            <div class="two columns">&nbsp;</div>
-            <button data-action="auto" class="zb-ctrl-btn four columns">
-                AUTO PILOT
-            </button>
-            <button data-action="stop" class="zb-ctrl-btn four columns">
-                <i class="fa fa-stop"></i>
-            </button>
-            <div class="two columns">&nbsp;</div>
-        </div>
-    </div>
-</div>
-
diff --git a/platypush/backend/http/templates/plugins/media.html b/platypush/backend/http/templates/plugins/media.html
deleted file mode 100644
index ec1ca751..00000000
--- a/platypush/backend/http/templates/plugins/media.html
+++ /dev/null
@@ -1,154 +0,0 @@
-<script type="text/javascript" src="{{ url_for('static', filename='js/media.js') }}"></script>
-<link rel="stylesheet" href="{{ url_for('static', filename='css/media.css') }}"></script>
-<link rel="stylesheet" href="{{ url_for('static', filename='flag-icons/css/flag-icon.min.css') }}"></script>
-
-<script type="text/javascript">
-    window.config = window.config || {};
-    window.config.media = window.config.media || {};
-    window.config.media.subtitles = JSON.parse('{{ utils.to_json(utils.get_config("media.subtitles")) | safe }}');
-</script>
-
-<div class="row" id="video-container">
-    <form action="#" id="video-search">
-        <div class="row">
-            <label for="video-search-text">
-                Supported formats: <tt>file://[path]</tt>, <tt>https://www.youtube.com/?v=[video_id]</tt>, or free text search
-            </label>
-        </div>
-
-        <div class="row">
-            <div class="nine columns" id="media-search-bar-container">
-                <input type="text" name="video-search-text" placeholder="Search query or video URL">
-            </div>
-            <div class="three columns" id="media-btns-container">
-                <button type="submit">
-                    <i class="fa fa-search"></i>
-                </button>
-                <button data-panel="#media-devices-panel">
-                    <i id="media-devices-panel-icon" class="fa fa-desktop"></i>
-                </button>
-            </div>
-        </div>
-
-        <div class="row panel" id="media-devices-panel">
-            <div class="refresh-devices-container">
-                <div class="row refresh-devices disabled">
-                    <i class="fa fa-retweet"></i>
-                </div>
-            </div>
-
-            <div class="devices-list">
-                <div class="row cast-device cast-device-local selected" data-local="local" data-name="_server">
-                    <div class="two columns">
-                        <i class="fa fa-desktop cast-device-icon"></i>
-                    </div>
-                    <div class="ten columns">
-                        <span class="cast-device-name">{{ utils.get_config('device_id') }}</span>
-                    </div>
-                </div>
-
-                <div class="row cast-device cast-device-local" data-browser="browser" data-name="_local">
-                    <div class="two columns">
-                        <i class="fa fa-laptop cast-device-icon"></i>
-                    </div>
-                    <div class="ten columns">
-                        <span class="cast-device-name">Browser</span>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </form>
-
-    <form action="#" id="video-ctrl">
-        <!-- <div class="row"> -->
-        <!--     <div class="eight columns offset-by-two slider-container" id="video-seeker-container"> -->
-        <!--         <span class="seek-time" id="video-elapsed">-:--</span>&nbsp; -->
-        <!--         <input type="range" min="0" id="video-seeker" disabled="disabled" class="slider" style="width:75%"> -->
-        <!--         &nbsp;<span class="seek-time" id="video-length">-:--</span> -->
-        <!--     </div> -->
-        <!-- </div> -->
-
-        <div class="row">
-            <div class="ten columns offset-by-one">
-                <button data-action="previous">
-                    <i class="fa fa-step-backward"></i>
-                </button>
-
-                <button data-action="back">
-                    <i class="fa fa-backward"></i>
-                </button>
-
-                <button data-action="pause">
-                    <i class="fa fa-pause"></i>
-                </button>
-
-                <button data-action="stop">
-                    <i class="fa fa-stop"></i>
-                </button>
-
-                <button data-action="forward">
-                    <i class="fa fa-forward"></i>
-                </button>
-
-                <button data-action="next">
-                    <i class="fa fa-step-forward"></i>
-                </button>
-
-                <button data-action="subtitles" data-modal="#media-subtitles-modal">
-                    <i class="fa fa-comment"></i>
-                </button>
-            </div>
-        </div>
-
-        <div class="row">
-            <div class="eight columns offset-by-two slider-container" id="video-volume-ctrl-container">
-                <i class="fa fa-volume-down"></i> &nbsp;
-                <input type="range" min="0" max="100" value="100" id="video-volume-ctrl" class="slider" style="width:80%">
-                &nbsp; <i class="fa fa-volume-up"></i>
-            </div>
-        </div>
-    </form>
-
-    <div class="row panel" id="media-item-panel">
-        <div class="row media-item-action" data-action="play">
-            <div class="two columns media-item-icon">
-                <i class="fa fa-play"></i>
-            </div>
-            <div class="ten columns">Play</div>
-        </div>
-
-        <div class="row media-item-action" data-action="download">
-            <div class="two columns">
-                <i class="fa fa-download"></i>
-            </div>
-            <div class="ten columns">Download</div>
-        </div>
-    </div>
-
-    <div id="media-subtitles-modal" class="modal">
-        <div class="modal-container">
-            <div class="modal-header">
-                Subtitles results
-            </div>
-
-            <div class="modal-body">
-                <div class="row media-subtitles-results-container">
-                    <div class="row media-subtitles-results-header">
-                        <div class="one column">Lang</div>
-                        <div class="five columns">Movie</div>
-                        <div class="six columns">Subtitles</div>
-                    </div>
-
-                    <div class="row media-subtitles-results">
-                    </div>
-                </div>
-                <div class="row media-subtitles-message">No media selected or playing</div>
-            </div>
-        </div>
-    </div>
-
-    <div class="row" id="video-results-container">
-        <div id="video-results"></div>
-    </div>
-</div>
-
diff --git a/platypush/backend/http/templates/plugins/media.mplayer.html b/platypush/backend/http/templates/plugins/media.mplayer.html
deleted file mode 120000
index b1a53826..00000000
--- a/platypush/backend/http/templates/plugins/media.mplayer.html
+++ /dev/null
@@ -1 +0,0 @@
-media.html
\ No newline at end of file
diff --git a/platypush/backend/http/templates/plugins/media.mpv.html b/platypush/backend/http/templates/plugins/media.mpv.html
deleted file mode 120000
index b1a53826..00000000
--- a/platypush/backend/http/templates/plugins/media.mpv.html
+++ /dev/null
@@ -1 +0,0 @@
-media.html
\ No newline at end of file
diff --git a/platypush/backend/http/templates/plugins/media.omxplayer.html b/platypush/backend/http/templates/plugins/media.omxplayer.html
deleted file mode 120000
index b1a53826..00000000
--- a/platypush/backend/http/templates/plugins/media.omxplayer.html
+++ /dev/null
@@ -1 +0,0 @@
-media.html
\ No newline at end of file
diff --git a/platypush/backend/http/templates/plugins/media.vlc.html b/platypush/backend/http/templates/plugins/media.vlc.html
deleted file mode 120000
index b1a53826..00000000
--- a/platypush/backend/http/templates/plugins/media.vlc.html
+++ /dev/null
@@ -1 +0,0 @@
-media.html
\ No newline at end of file
diff --git a/platypush/backend/http/templates/plugins/serial.html b/platypush/backend/http/templates/plugins/serial.html
deleted file mode 120000
index 20963d36..00000000
--- a/platypush/backend/http/templates/plugins/serial.html
+++ /dev/null
@@ -1 +0,0 @@
-gpio.sensor.mcp3008.html
\ No newline at end of file
diff --git a/platypush/backend/http/templates/plugins/switch.switchbot.html b/platypush/backend/http/templates/plugins/switch.switchbot.html
deleted file mode 100644
index a0113fce..00000000
--- a/platypush/backend/http/templates/plugins/switch.switchbot.html
+++ /dev/null
@@ -1,17 +0,0 @@
-<script type="text/javascript" src="{{ url_for('static', filename='js/switch.switchbot.js') }}"></script>
-<link rel="stylesheet" href="{{ url_for('static', filename='css/switch.switchbot.css') }}"></script>
-
-<div id="switchbot-container" class="row">
-    {% for addr, name in configuration['devices'].items() %}
-        <div class="row switchbot-device" data-addr="{{ addr }}">
-            <div class="ten columns name">{{ name }}</div>
-            <div class="two columns toggle-container">
-                <div class="toggle toggle--push switch-ctrl-container">
-                    <label for="{{ addr }}" class="toggle--btn"></label>
-                    <input type="checkbox" name="{{ addr }}" class="toggle--checkbox switch-ctrl">
-                </div>
-            </div>
-        </div>
-    {% endfor %}
-</div>
-
diff --git a/platypush/backend/http/templates/plugins/switch.tplink.html b/platypush/backend/http/templates/plugins/switch.tplink.html
deleted file mode 100644
index ad4c74fb..00000000
--- a/platypush/backend/http/templates/plugins/switch.tplink.html
+++ /dev/null
@@ -1,5 +0,0 @@
-<script type="text/javascript" src="{{ url_for('static', filename='js/switch.tplink.js') }}"></script>
-<link rel="stylesheet" href="{{ url_for('static', filename='css/switch.tplink.css') }}"></script>
-
-<div id="tplink-container" class="row"></div>
-
diff --git a/platypush/backend/http/templates/plugins/switch.wemo.html b/platypush/backend/http/templates/plugins/switch.wemo.html
deleted file mode 100644
index 94b5754f..00000000
--- a/platypush/backend/http/templates/plugins/switch.wemo.html
+++ /dev/null
@@ -1,5 +0,0 @@
-<script type="text/javascript" src="{{ url_for('static', filename='js/switch.wemo.js') }}"></script>
-<link rel="stylesheet" href="{{ url_for('static', filename='css/switch.wemo.css') }}"></script>
-
-<div id="wemo-container" class="row"></div>
-
diff --git a/platypush/backend/http/templates/widgets/calendar.html b/platypush/backend/http/templates/widgets/calendar.html
deleted file mode 100644
index 1f2bdf3d..00000000
--- a/platypush/backend/http/templates/widgets/calendar.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<script type="text/javascript" src="{{ url_for('static', filename='js/widgets/calendar.js') }}"></script>
-<link rel="stylesheet" href="{{ url_for('static', filename='css/widgets/calendar.css') }}"></script>
-
-<div class="calendar-container">
-    <div class="calendar-next-event-container">
-        <div class="date"></div>
-        <div class="summary"></div>
-        <div class="time"></div>
-    </div>
-
-    <div class="calendar-events-list-container"></div>
-</div>
-
diff --git a/platypush/backend/http/templates/widgets/calendar/index.html b/platypush/backend/http/templates/widgets/calendar/index.html
new file mode 100644
index 00000000..2be6e8d4
--- /dev/null
+++ b/platypush/backend/http/templates/widgets/calendar/index.html
@@ -0,0 +1,15 @@
+<script type="text/x-template" id="tmpl-widget-calendar">
+    <div class="calendar">
+        <div class="event upcoming-event" v-if="events.length > 0">
+            <div class="date" v-text="formatDate(events[0].start)"></div>
+            <div class="summary" v-text="events[0].summary"></div>
+            <div class="time">{% raw %}{{ formatTime(events[0].start) }} - {{ formatTime(events[0].end) }}{% endraw %}</div>
+        </div>
+
+        <div class="event" v-for="event in events.slice(1)" v-if="events.length > 1" :key="event.id">
+            <div class="date col-2" v-text="formatDate(event.start)"></div>
+            <div class="time col-2" v-text="formatTime(event.start)"></div>
+            <div class="summary col-8" v-text="event.summary"></div>
+        </div>
+    </div>
+</script>
diff --git a/platypush/backend/http/templates/widgets/date-time-weather.html b/platypush/backend/http/templates/widgets/date-time-weather.html
deleted file mode 100644
index a43ebb44..00000000
--- a/platypush/backend/http/templates/widgets/date-time-weather.html
+++ /dev/null
@@ -1,28 +0,0 @@
-<script type="text/javascript" src="{{ url_for('static', filename='js/widgets/skycons.js') }}"></script>
-<script type="text/javascript" src="{{ url_for('static', filename='js/widgets/date-time-weather.js') }}"></script>
-<link rel="stylesheet" href="{{ url_for('static', filename='css/widgets/date-time-weather.css') }}"></script>
-
-<div class="date-time-weather-container">
-    <div class="date" data-bind="date"></div>
-    <div class="time" data-bind="time"></div>
-
-    <h1 class="temperature">
-        <canvas id="weather-icon" width="50" height="50"></canvas> &nbsp;
-        <span data-bind="temperature">N/A</span>&deg;
-    </h1>
-
-    <div class="forecast" data-bind="forecast"></div>
-
-    <div class="sensors row">
-        <div class="sensor-temperature six columns">
-            <i class="fa fa-thermometer"></i> &nbsp;
-            <span data-bind="sensor-temperature">N/A</span>&deg;
-        </div>
-
-        <div class="sensor-humidity six columns">
-            <i class="fa fa-tint"></i> &nbsp;
-            <span data-bind="sensor-humidity">N/A</span>%
-        </div>
-    </div>
-</div>
-
diff --git a/platypush/backend/http/templates/widgets/date-time-weather/index.html b/platypush/backend/http/templates/widgets/date-time-weather/index.html
new file mode 100644
index 00000000..866e119a
--- /dev/null
+++ b/platypush/backend/http/templates/widgets/date-time-weather/index.html
@@ -0,0 +1,39 @@
+<script type="text/javascript" src="{{ url_for('static', filename='js/widgets/date-time-weather/skycons.js') }}"></script>
+
+<script type="text/x-template" id="tmpl-widget-date-time-weather">
+    <div class="date-time-weather">
+        <div class="date" v-text="formatDate(now)"></div>
+        <div class="time" v-text="formatTime(now)"></div>
+
+        <h1 class="weather">
+            <canvas id="weather-icon" width="50" height="50"></canvas> &nbsp;
+            <span class="temperature" v-if="weather">
+                {% raw %}
+                    {{ Math.round(parseFloat(weather.temperature)) + '&deg;' }}
+                {% endraw %}
+            </span>
+        </h1>
+
+        <div class="summary" v-if="weather && weather.summary" v-text="weather.summary"></div>
+
+        <div class="sensors" v-if="Object.keys(sensors).length">
+            <div class="sensor temperature col-6" v-if="sensors.temperature">
+                <i class="fas fa-thermometer-half"></i> &nbsp;
+                <span class="temperature">
+                    {% raw %}
+                        {{ parseFloat(sensors.temperature).toFixed(1) + '&deg;'}}
+                    {% endraw %}
+                </span>
+            </div>
+
+            <div class="sensor humidity col-6" v-if="sensors.humidity">
+                <i class="fa fa-tint"></i> &nbsp;
+                <span class="humidity">
+                    {% raw %}
+                        {{ parseFloat(sensors.humidity).toFixed(1) + '%'}}
+                    {% endraw %}
+                </span>
+            </div>
+        </div>
+    </div>
+</script>
diff --git a/platypush/backend/http/templates/widgets/image-carousel.html b/platypush/backend/http/templates/widgets/image-carousel.html
deleted file mode 100644
index 8ab899b5..00000000
--- a/platypush/backend/http/templates/widgets/image-carousel.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<script type="text/javascript" src="{{ url_for('static', filename='js/widgets/image-carousel.js') }}"></script>
-<link rel="stylesheet" href="{{ url_for('static', filename='css/widgets/image-carousel.css') }}"></script>
-
-{% set images = utils.search_web_directory(
-    widget['images_path'], '.jpg', 'jpeg', '.png') %}
-
-<script type="text/javascript">
-    window.widgets['image-carousel'].imageUrls = {{ images|safe }};
-</script>
-
-<div class="carousel">
-    <div class="carousel-background">&nbsp;</div>
-    <img alt="Your carousel images">
-</div>
-
diff --git a/platypush/backend/http/templates/widgets/image-carousel/index.html b/platypush/backend/http/templates/widgets/image-carousel/index.html
new file mode 100644
index 00000000..4768849c
--- /dev/null
+++ b/platypush/backend/http/templates/widgets/image-carousel/index.html
@@ -0,0 +1,6 @@
+<script type="text/x-template" id="tmpl-widget-image-carousel">
+    <div class="image-carousel">
+        <div ref="background" class="background"></div>
+        <img ref="img" :src="currentImage" alt="Your carousel images">
+    </div>
+</script>
diff --git a/platypush/backend/http/templates/widgets/music.html b/platypush/backend/http/templates/widgets/music.html
deleted file mode 100644
index 40e64f9c..00000000
--- a/platypush/backend/http/templates/widgets/music.html
+++ /dev/null
@@ -1,63 +0,0 @@
-<script type="text/javascript" src="{{ url_for('static', filename='js/widgets/music.js') }}"></script>
-<link rel="stylesheet" href="{{ url_for('static', filename='css/widgets/music.css') }}"></script>
-
-<div class="music-container">
-    <div class="track-container">
-        <div class="no-track-info">No media is being played</div>
-        <div class="track-info">
-            <div class="artist" data-bind="artist"></div>
-            <div class="title" data-bind="title"></div>
-        </div>
-    </div>
-
-    <div class="time-container">
-        <div class="row">
-            <div class="time-bar">
-                <div class="elapsed"></div>
-            </div>
-        </div>
-
-        <div class="row">
-            <div class="six columns">
-                <div class="time-elapsed" data-bind="time-elapsed"></div>
-            </div>
-
-            <div class="six columns">
-                <div class="time-total" data-bind="time-total"></div>
-            </div>
-        </div>
-    </div>
-
-    <div class="playback-status-container">
-        <div class="row playback-status-titles">
-            <div class="two columns offset-by-one">Volume</div>
-            <div class="two columns">Random</div>
-            <div class="two columns">Repeat</div>
-            <div class="two columns">Single</div>
-            <div class="two columns">Consume</div>
-        </div>
-
-        <div class="row playback-status-values">
-            <div class="two columns offset-by-one">
-                <div data-bind="playback-volume"></div>
-            </div>
-
-            <div class="two columns">
-                <div data-bind="playback-random"></div>
-            </div>
-
-            <div class="two columns">
-                <div data-bind="playback-repeat"></div>
-            </div>
-
-            <div class="two columns">
-                <div data-bind="playback-single"></div>
-            </div>
-
-            <div class="two columns">
-                <div data-bind="playback-consume"></div>
-            </div>
-        </div>
-    </div>
-</div>
-
diff --git a/platypush/backend/http/templates/widgets/music/index.html b/platypush/backend/http/templates/widgets/music/index.html
new file mode 100644
index 00000000..08f59925
--- /dev/null
+++ b/platypush/backend/http/templates/widgets/music/index.html
@@ -0,0 +1,44 @@
+<script type="text/x-template" id="tmpl-widget-music">
+    <div class="music">
+        <div class="track">
+            <div class="unknown" v-if="!status">[Unknown state]</div>
+            <div class="no-track" v-if="status && status.state === 'stop'">No media is being played</div>
+            <div class="artist" v-if="status && status.state !== 'stop' && track && track.artist" v-text="track.artist"></div>
+            <div class="title" v-if="status && status.state !== 'stop' && track && track.title" v-text="track.title"></div>
+        </div>
+
+        <div class="time"  v-if="status && status.state === 'play'">
+            <div class="row">
+                <div class="progress-bar">
+                    <div class="elapsed" :style="{width: track.time ? 100*(status.elapsed/track.time) + '%' : '100%'}"></div>
+                    <div class="total"></div>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-6 time-elapsed" v-text="convertTime(status.elapsed)"></div>
+                <div class="col-6 time-total" v-if="track.time" v-text="convertTime(track.time)"></div>
+            </div>
+        </div>
+
+        <div class="playback-status" v-if="status">
+            <div class="status-property col-4">
+                <i class="fa fa-volume-up"></i>&nbsp; <span v-text="status.volume + '%'"></span>
+            </div>
+
+            <div class="status-property col-2">
+                <i class="fas fa-random" :class="{active: status.random}"></i>
+            </div>
+            <div class="status-property col-2">
+                <i class="fas fa-redo" :class="{active: status.repeat}"></i>
+            </div>
+            <div class="status-property col-2">
+                <i class="fa fa-bullseye" :class="{active: status.single}"></i>
+            </div>
+            <div class="status-property col-2">
+                <i class="fa fa-utensils" :class="{active: status.consume}"></i>
+            </div>
+        </div>
+    </div>
+</script>
+
diff --git a/platypush/backend/http/templates/widgets/rss-news.html b/platypush/backend/http/templates/widgets/rss-news.html
deleted file mode 100644
index adef9603..00000000
--- a/platypush/backend/http/templates/widgets/rss-news.html
+++ /dev/null
@@ -1,5 +0,0 @@
-<script type="text/javascript" src="{{ url_for('static', filename='js/widgets/rss-news.js') }}"></script>
-<link rel="stylesheet" href="{{ url_for('static', filename='css/widgets/rss-news.css') }}"></script>
-
-<div class="news-container"></div>
-
diff --git a/platypush/backend/http/templates/widgets/rss-news/index.html b/platypush/backend/http/templates/widgets/rss-news/index.html
new file mode 100644
index 00000000..aad0a9fb
--- /dev/null
+++ b/platypush/backend/http/templates/widgets/rss-news/index.html
@@ -0,0 +1,9 @@
+<script type="text/x-template" id="tmpl-widget-rss-news">
+    <div class="rss-news">
+        <div class="article" v-if="currentArticle">
+            <div class="source" v-text="currentArticle.source"></div>
+            <div class="title" v-text="currentArticle.title"></div>
+            <div class="published" v-text="new Date(currentArticle.published).toDateString() + ', ' + new Date(currentArticle.published).toTimeString().substring(0,5)"></div>
+        </div>
+    </div>
+</script>
diff --git a/platypush/backend/http/templates/widgets/template.html b/platypush/backend/http/templates/widgets/template.html
new file mode 100644
index 00000000..1ace1c38
--- /dev/null
+++ b/platypush/backend/http/templates/widgets/template.html
@@ -0,0 +1,6 @@
+<script type="text/x-template" id="tmpl-widget">
+    <div class="widget" :class="'col-' + config.columns">
+        <component :is="tag" :config="config"></component>
+    </div>
+</script>
+
diff --git a/platypush/backend/http/utils.py b/platypush/backend/http/utils.py
index f509f45c..71ed46ee 100644
--- a/platypush/backend/http/utils.py
+++ b/platypush/backend/http/utils.py
@@ -13,35 +13,16 @@ class HttpUtils(object):
     @staticmethod
     def widget_columns_to_html_class(columns):
         if not isinstance(columns, int):
-            raise RuntimeError('columns should be a number, got "{}"'.format(columns))
+            try:
+                columns = int(columns)
+            except ValueError:
+                raise RuntimeError('columns should be a number, got {} ({})'.format(type(columns), columns))
 
-        if columns == 1:
-            return 'one column'
-        elif columns == 2:
-            return 'two columns'
-        elif columns == 3:
-            return 'three columns'
-        elif columns == 4:
-            return 'four columns'
-        elif columns == 5:
-            return 'five columns'
-        elif columns == 6:
-            return 'six columns'
-        elif columns == 7:
-            return 'seven columns'
-        elif columns == 8:
-            return 'eight columns'
-        elif columns == 9:
-            return 'nine columns'
-        elif columns == 10:
-            return 'ten columns'
-        elif columns == 11:
-            return 'eleven columns'
-        elif columns == 12:
-            return 'twelve columns'
-        else:
-            raise RuntimeError('Constraint violation: should be 1 <= columns <= 12, ' +
-                               'got columns={}'.format(columns))
+        if 1 <= columns <= 12:
+            return 'col-{}'.format(columns)
+
+        raise RuntimeError('Constraint violation: should be 1 <= columns <= 12, ' +
+                           'got columns={}'.format(columns))
 
     @staticmethod
     def search_directory(directory, *extensions, recursive=False):
@@ -127,5 +108,8 @@ class HttpUtils(object):
         with open(file) as f:
             return f.read()
 
+    @classmethod
+    def isfile(cls, file):
+        return os.path.isfile(file)
 
 # vim:sw=4:ts=4:et:
diff --git a/platypush/plugins/utils.py b/platypush/plugins/utils.py
index 12ab7519..482dc8bc 100644
--- a/platypush/plugins/utils.py
+++ b/platypush/plugins/utils.py
@@ -2,6 +2,7 @@ import json
 import threading
 import time
 
+from platypush.backend.http.utils import HttpUtils
 from platypush.plugins import Plugin, action
 from platypush.procedure import Procedure
 
@@ -313,4 +314,13 @@ class UtilsPlugin(Plugin):
                 }
             }
 
+    @action
+    def search_directory(self, directory, extensions, recursive=False):
+        return HttpUtils.search_directory(directory, recursive=recursive, *extensions)
+
+    @action
+    def search_web_directory(self, directory, extensions):
+        return HttpUtils.search_web_directory(directory, *extensions)
+
+
 # vim:sw=4:ts=4:et: