changeset 602:c6bc0ef03f82

admin: Better UI for publishing websites. * Support multiple publish targets. * Dedicated UI for publishing. * Some UI polish.
author Ludovic Chabant <ludovic@chabant.com>
date Wed, 27 Jan 2016 18:02:25 -0800
parents effbc78b5528
children 7e4e567377cd
files foodtruck/main.py foodtruck/pubutil.py foodtruck/sites.py foodtruck/templates/dashboard.html foodtruck/templates/error.html foodtruck/templates/layouts/master.html foodtruck/templates/publish.html foodtruck/views/baking.py foodtruck/views/dashboard.py foodtruck/views/menu.py foodtruck/views/publish.py foodtruck/web.py foodtruckui/assets/js/foodtruck.js foodtruckui/assets/sass/foodtruck/_base.scss
diffstat 14 files changed, 280 insertions(+), 218 deletions(-) [+]
line wrap: on
line diff
--- a/foodtruck/main.py	Sun Jan 24 10:42:33 2016 -0800
+++ b/foodtruck/main.py	Wed Jan 27 18:02:25 2016 -0800
@@ -16,8 +16,8 @@
         # This is needed for Werkzeug's code reloader to be able to correctly
         # shutdown the child process in order to restart it (otherwise, SSE
         # generators will keep it alive).
-        from .views import baking
+        from . import pubutil
         logger.debug("Shutting down SSE generators from main...")
-        baking.server_shutdown = True
+        pubutil.server_shutdown = True
         raise
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/foodtruck/pubutil.py	Wed Jan 27 18:02:25 2016 -0800
@@ -0,0 +1,93 @@
+import os
+import os.path
+import time
+import signal
+import logging
+from .web import app
+
+
+logger = logging.getLogger(__name__)
+
+server_shutdown = False
+
+
+def _shutdown_server_and_raise_sigint():
+    if not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
+        # This is needed when hitting CTRL+C to shutdown the Werkzeug server,
+        # otherwise SSE generators will keep it alive.
+        logger.debug("Shutting down SSE generators...")
+        global server_shutdown
+        server_shutdown = True
+    raise KeyboardInterrupt()
+
+
+if app.config['FOODTRUCK_CMDLINE_MODE']:
+    # Make sure CTRL+C works correctly.
+    signal.signal(signal.SIGINT,
+                  lambda *args: _shutdown_server_and_raise_sigint())
+
+
+class PublishLogReader(object):
+    _pub_max_time = 10 * 60   # Don't bother about pubs older than 10mins.
+    _poll_interval = 2        # Check the PID file every 2 seconds.
+    _ping_interval = 30       # Send a ping message every 30 seconds.
+
+    def __init__(self, pid_path, log_path):
+        self.pid_path = pid_path
+        self.log_path = log_path
+        self._pub_pid_mtime = 0
+        self._last_seek = 0
+        self._last_ping_time = 0
+
+    def run(self):
+        logger.debug("Opening publish log...")
+
+        try:
+            while not server_shutdown:
+                # PING!
+                interval = time.time() - self._last_ping_time
+                if interval > self._ping_interval:
+                    logger.debug("Sending ping...")
+                    self._last_ping_time = time.time()
+                    yield bytes("event: ping\ndata: 1\n\n", 'utf8')
+
+                # Check pid file.
+                prev_mtime = self._pub_pid_mtime
+                try:
+                    self._pub_pid_mtime = os.path.getmtime(self.pid_path)
+                    if time.time() - self._pub_pid_mtime > \
+                            self._pub_max_time:
+                        self._pub_pid_mtime = 0
+                except OSError:
+                    self._pub_pid_mtime = 0
+
+                # Send data.
+                new_data = None
+                if self._pub_pid_mtime > 0 or prev_mtime > 0:
+                    if self._last_seek == 0:
+                        outstr = 'event: message\ndata: Publish started.\n\n'
+                        yield bytes(outstr, 'utf8')
+
+                    try:
+                        with open(self.log_path, 'r', encoding='utf8') as fp:
+                            fp.seek(self._last_seek)
+                            new_data = fp.read()
+                            self._last_seek = fp.tell()
+                    except OSError:
+                        pass
+                if self._pub_pid_mtime == 0:
+                    self._last_seek = 0
+
+                if new_data:
+                    logger.debug("SSE: %s" % outstr)
+                    for line in new_data.split('\n'):
+                        outstr = 'event: message\ndata: %s\n\n' % line
+                        yield bytes(outstr, 'utf8')
+
+                time.sleep(self._poll_interval)
+
+        except GeneratorExit:
+            pass
+
+        logger.debug("Closing publish log...")
+
--- a/foodtruck/sites.py	Sun Jan 24 10:42:33 2016 -0800
+++ b/foodtruck/sites.py	Wed Jan 27 18:02:25 2016 -0800
@@ -6,7 +6,7 @@
 import threading
 import subprocess
 from piecrust.app import PieCrust
-from piecrust.configuration import merge_dicts
+from piecrust.configuration import merge_dicts, Configuration
 
 
 logger = logging.getLogger(__name__)
@@ -24,10 +24,11 @@
     def __init__(self, name, root_dir, config):
         self.name = name
         self.root_dir = root_dir
-        self.config = config
+        self.config = Configuration(values=config.get('sites/%s' % name, {}))
+        self._global_config = config
         self._piecrust_app = None
         self._scm = None
-        self._bake_thread = None
+        self._publish_thread = None
         logger.debug("Creating site object for %s" % self.name)
 
     @property
@@ -41,70 +42,70 @@
     @property
     def scm(self):
         if self._scm is None:
-            cfg = None
-            scm_cfg = self.config.get('sites/%s/scm' % self.name)
-            global_scm_cfg = self.config.get('scm')
-            if scm_cfg:
-                if global_scm_cfg:
-                    cfg = copy.deepcopy(global_scm_cfg)
-                    merge_dicts(cfg, scm_cfg)
-                else:
-                    cfg = copy.deepcopy(scm_cfg)
-            elif global_scm_cfg:
-                cfg = copy.deepcopy(global_scm_cfg)
+            cfg = copy.deepcopy(self._global_config.get('scm', {}))
+            merge_dicts(cfg, self.config.get('scm', {}))
 
-            if not cfg or 'type' not in cfg:
-                raise Exception("No SCM available for site: %s" % self.name)
-
-            if cfg['type'] == 'hg':
+            if os.path.isdir(os.path.join(self.root_dir, '.hg')):
                 from .scm.mercurial import MercurialSourceControl
                 self._scm = MercurialSourceControl(self.root_dir, cfg)
+            elif os.path.isdir(os.path.join(self.root_dir, '.git')):
+                from .scm.git import GitSourceControl
+                self._scm = GitSourceControl(self.root_dir, cfg)
             else:
-                raise NotImplementedError()
+                self._scm = False
 
         return self._scm
 
     @property
-    def is_bake_running(self):
-        return self._bake_thread is not None and self._bake_thread.is_alive()
+    def is_publish_running(self):
+        return (self._publish_thread is not None and
+                self._publish_thread.is_alive())
 
     @property
-    def bake_thread(self):
-        return self._bake_thread
+    def publish_thread(self):
+        return self._publish_thread
+
+    def publish(self, target):
+        target_cfg = self.config.get('publish/%s' % target)
+        if not target_cfg:
+            raise Exception("No such publish target: %s" % target)
 
-    def bake(self):
-        bake_cmd = self.config.get('triggers/bake')
-        bake_args = shlex.split(bake_cmd)
+        target_cmd = target_cfg.get('cmd')
+        if not target_cmd:
+            raise Exception("No command specified for publish target: %s" %
+                            target)
+        publish_args = shlex.split(target_cmd)
 
-        logger.debug("Running bake: %s" % bake_args)
-        proc = subprocess.Popen(bake_args, cwd=self.root_dir,
+        logger.debug(
+                "Executing publish target '%s': %s" % (target, publish_args))
+        proc = subprocess.Popen(publish_args, cwd=self.root_dir,
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE)
 
-        pid_file_path = os.path.join(self.root_dir, 'foodtruck_bake.pid')
+        pid_file_path = os.path.join(self.root_dir, '.ft_pub.pid')
         with open(pid_file_path, 'w') as fp:
             fp.write(str(proc.pid))
 
-        logger.debug("Running bake monitor for PID %d" % proc.pid)
-        self._bake_thread = _BakeThread(self.name, self.root_dir, proc,
-                                        self._onBakeEnd)
-        self._bake_thread.start()
+        logger.debug("Running publishing monitor for PID %d" % proc.pid)
+        self._publish_thread = _PublishThread(
+                self.name, self.root_dir, proc, self._onPublishEnd)
+        self._publish_thread.start()
 
-    def _onBakeEnd(self):
-        os.unlink(os.path.join(self.root_dir, 'foodtruck_bake.pid'))
-        self._bake_thread = None
+    def _onPublishEnd(self):
+        os.unlink(os.path.join(self.root_dir, '.ft_pub.pid'))
+        self._publish_thread = None
 
 
-class _BakeThread(threading.Thread):
+class _PublishThread(threading.Thread):
     def __init__(self, sitename, siteroot, proc, callback):
-        super(_BakeThread, self).__init__(
-                name='%s_bake' % sitename, daemon=True)
+        super(_PublishThread, self).__init__(
+                name='%s_publish' % sitename, daemon=True)
         self.sitename = sitename
         self.siteroot = siteroot
         self.proc = proc
         self.callback = callback
 
-        log_file_path = os.path.join(self.siteroot, 'foodtruck_bake.log')
+        log_file_path = os.path.join(self.siteroot, '.ft_pub.log')
         self.log_fp = open(log_file_path, 'w', encoding='utf8')
 
     def run(self):
@@ -114,11 +115,11 @@
             self.log_fp.write(line.decode('utf8'))
         self.proc.communicate()
         if self.proc.returncode != 0:
-            self.log_fp.write("Error, bake process returned code %d" %
+            self.log_fp.write("Error, publish process returned code %d" %
                               self.proc.returncode)
         self.log_fp.close()
 
-        logger.debug("Bake ended for %s." % self.sitename)
+        logger.debug("Publish ended for %s." % self.sitename)
         self.callback()
 
 
--- a/foodtruck/templates/dashboard.html	Sun Jan 24 10:42:33 2016 -0800
+++ b/foodtruck/templates/dashboard.html	Wed Jan 27 18:02:25 2016 -0800
@@ -8,7 +8,9 @@
             <form action="{{url_switch}}" method="POST">
             {% for site in sites %}
                 {% if site.name != site_name %}
-                <button type="submit" name="site_name" value="{{site.name}}" class="btn">Switch to {{site.display_name}}</button>
+                    <button type="submit" name="site_name" value="{{site.name}}" class="btn">
+                    <span class="icon ion-shuffle"></span>
+                    Switch to {{site.display_name}}</button>
                 {% endif %}
             {% endfor %}
             </form>
@@ -55,15 +57,6 @@
 
         </div>
     </div>
-    <div class="row">
-        <div class="col-md-12">
-            <h2><span class="icon ion-radio-waves"></span> Publishing</h2>
-            <form action="{{url_bake}}" method="POST">
-                <button name="do_bake" type="submit" class="btn btn-primary btn-lg">
-                    <span class="icon ion-upload"></span> Bake!</button>
-            </form>
-        </div>
-    </div>
 </div>
 {% endblock %}
 
--- a/foodtruck/templates/error.html	Sun Jan 24 10:42:33 2016 -0800
+++ b/foodtruck/templates/error.html	Wed Jan 27 18:02:25 2016 -0800
@@ -4,9 +4,7 @@
 
 {% block content %}
 <div class="container">
-    {% if error == 'invalid_site' %}
-        <p>There was an error with your configuration file: {{exception}}</p>
-    {% endif %}
+    <p>{{error}}</p>
 </div>
 {% endblock %}
 
--- a/foodtruck/templates/layouts/master.html	Sun Jan 24 10:42:33 2016 -0800
+++ b/foodtruck/templates/layouts/master.html	Wed Jan 27 18:02:25 2016 -0800
@@ -21,7 +21,7 @@
                 {% block content %}{% endblock %}
             </section>
             {% block after_content %}{% endblock %}
-            <div id="ft-bakelog"></div>
+            <div id="ft-publog"></div>
             <footer>
                 <p>Prepared by <a href="http://bolt80.com">BOLT80</a>.</p>
                 <p>Much <span class="icon ion-heart"></span> to
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/foodtruck/templates/publish.html	Wed Jan 27 18:02:25 2016 -0800
@@ -0,0 +1,22 @@
+{% extends 'layouts/default.html' %}
+
+{% block content %}
+<div class="container">
+    <h1>Publish {{site_title}}</h1>
+
+    {% for target in targets %}
+    <div>
+        <h2>{{target.name}}</h2>
+        {% if target.description %}<div>{{target.description}}</div>{% endif %}
+        {% if target.cmd %}<p>Run <code>{{target.cmd}}</code>.</p>{% endif %}
+        <form action="{{url_run}}" method="POST">
+            <input type="hidden" name="target" value="{{target.name}}" />
+            <button type="submit" class="btn btn-default">Execute</button>
+        </form>
+    </div>
+    {% endfor %}
+</div>
+
+{% endblock %}
+
+
--- a/foodtruck/views/baking.py	Sun Jan 24 10:42:33 2016 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,117 +0,0 @@
-import os
-import os.path
-import time
-import signal
-import logging
-from werkzeug.wrappers import Response
-from flask import g, redirect
-from flask.ext.login import login_required
-from ..web import app
-
-
-logger = logging.getLogger(__name__)
-
-server_shutdown = False
-
-
-def _shutdown_server_and_raise_sigint():
-    if not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
-        # This is needed when hitting CTRL+C to shutdown the Werkzeug server,
-        # otherwise SSE generators will keep it alive.
-        logger.debug("Shutting down SSE generators...")
-        global server_shutdown
-        server_shutdown = True
-    raise KeyboardInterrupt()
-
-
-if app.config['FOODTRUCK_CMDLINE_MODE']:
-    # Make sure CTRL+C works correctly.
-    signal.signal(signal.SIGINT,
-                  lambda *args: _shutdown_server_and_raise_sigint())
-
-
-class _BakeLogReader(object):
-    _bake_max_time = 10 * 60  # Don't bother about bakes older than 10mins.
-    _poll_interval = 2        # Check the PID file every 2 seconds.
-    _ping_interval = 30       # Send a ping message every 30 seconds.
-
-    def __init__(self, pid_path, log_path):
-        self.pid_path = pid_path
-        self.log_path = log_path
-        self._bake_pid_mtime = 0
-        self._last_seek = 0
-        self._last_ping_time = 0
-
-    def run(self):
-        logger.debug("Opening bake log...")
-
-        try:
-            while not server_shutdown:
-                # PING!
-                interval = time.time() - self._last_ping_time
-                if interval > self._ping_interval:
-                    logger.debug("Sending ping...")
-                    self._last_ping_time = time.time()
-                    yield bytes("event: ping\ndata: 1\n\n", 'utf8')
-
-                # Check pid file.
-                prev_mtime = self._bake_pid_mtime
-                try:
-                    self._bake_pid_mtime = os.path.getmtime(self.pid_path)
-                    if time.time() - self._bake_pid_mtime > \
-                            self._bake_max_time:
-                        self._bake_pid_mtime = 0
-                except OSError:
-                    self._bake_pid_mtime = 0
-
-                # Send data.
-                new_data = None
-                if self._bake_pid_mtime > 0 or prev_mtime > 0:
-                    if self._last_seek == 0:
-                        outstr = 'event: message\ndata: Bake started.\n\n'
-                        yield bytes(outstr, 'utf8')
-
-                    try:
-                        with open(self.log_path, 'r', encoding='utf8') as fp:
-                            fp.seek(self._last_seek)
-                            new_data = fp.read()
-                            self._last_seek = fp.tell()
-                    except OSError:
-                        pass
-                if self._bake_pid_mtime == 0:
-                    self._last_seek = 0
-
-                if new_data:
-                    logger.debug("SSE: %s" % outstr)
-                    for line in new_data.split('\n'):
-                        outstr = 'event: message\ndata: %s\n\n' % line
-                        yield bytes(outstr, 'utf8')
-
-                time.sleep(self._poll_interval)
-
-        except GeneratorExit:
-            pass
-
-        logger.debug("Closing bake log...")
-
-
-@app.route('/bake', methods=['POST'])
-@login_required
-def bake_site():
-    site = g.sites.get()
-    site.bake()
-    return redirect('/')
-
-
-@app.route('/bakelog')
-@login_required
-def stream_bake_log():
-    site = g.sites.get()
-    pid_path = os.path.join(site.root_dir, 'foodtruck_bake.pid')
-    log_path = os.path.join(site.root_dir, 'foodtruck_bake.log')
-    rdr = _BakeLogReader(pid_path, log_path)
-
-    response = Response(rdr.run(), mimetype='text/event-stream')
-    response.headers['Cache-Control'] = 'no-cache'
-    return response
-
--- a/foodtruck/views/dashboard.py	Sun Jan 24 10:42:33 2016 -0800
+++ b/foodtruck/views/dashboard.py	Wed Jan 27 18:02:25 2016 -0800
@@ -54,7 +54,7 @@
 
     data['site_name'] = site.name
     data['site_title'] = site.piecrust_app.config.get('site/title', site.name)
-    data['url_bake'] = url_for('bake_site')
+    data['url_publish'] = url_for('publish')
     data['url_preview'] = url_for('preview_site_root', sitename=site.name)
 
     data['sites'] = []
--- a/foodtruck/views/menu.py	Sun Jan 24 10:42:33 2016 -0800
+++ b/foodtruck/views/menu.py	Wed Jan 27 18:02:25 2016 -0800
@@ -14,7 +14,7 @@
         if s.is_theme_source:
             continue
 
-        source_icon = s.config.get('foodtruck_icon/document')
+        source_icon = s.config.get('admin_icon', 'document')
         if s.name == 'pages':
             source_icon = 'document-text'
         elif 'blog' in s.name:
@@ -34,6 +34,11 @@
                 }
         entries.append(ctx)
 
+    entries.append({
+        'url': url_for('publish'),
+        'title': "Publish",
+        'icon': 'upload'})
+
     # entries.append({
     #     'url': url_for('settings'),
     #     'title': "Settings",
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/foodtruck/views/publish.py	Wed Jan 27 18:02:25 2016 -0800
@@ -0,0 +1,63 @@
+import os
+import os.path
+import copy
+import logging
+from flask import request, g, url_for, render_template, Response
+from flask.ext.login import login_required
+from piecrust.configuration import merge_dicts
+from ..pubutil import PublishLogReader
+from ..views import with_menu_context
+from ..web import app
+
+
+logger = logging.getLogger(__name__)
+
+
+@app.route('/publish', methods=['GET', 'POST'])
+@login_required
+def publish():
+    if request.method == 'POST':
+        target = request.form.get('target')
+        if not target:
+            raise Exception("No target specified.")
+
+        site = g.sites.get()
+        site.publish(target)
+
+    site = g.sites.get()
+    pub_cfg = copy.deepcopy(g.config.get('publish', {}))
+    merge_dicts(pub_cfg, site.config.get('publish', {}))
+    if not pub_cfg:
+        data = {'error': "There are not publish targets defined in your "
+                         "configuration file."}
+        return render_template('error.html', **data)
+
+    data = {}
+    data['url_run'] = url_for('publish')
+    data['site_title'] = site.piecrust_app.config.get('site/title', site.name)
+    data['targets'] = []
+    for tn in sorted(pub_cfg.keys()):
+        tc = pub_cfg[tn]
+        data['targets'].append({
+            'name': tn,
+            'description': tc.get('description'),
+            'cmd': tc.get('cmd')
+            })
+
+    with_menu_context(data)
+
+    return render_template('publish.html', **data)
+
+
+@app.route('/publish-log')
+@login_required
+def stream_publish_log():
+    site = g.sites.get()
+    pid_path = os.path.join(site.root_dir, '.ft_pub.pid')
+    log_path = os.path.join(site.root_dir, '.ft_pub.log')
+    rdr = PublishLogReader(pid_path, log_path)
+
+    response = Response(rdr.run(), mimetype='text/event-stream')
+    response.headers['Cache-Control'] = 'no-cache'
+    return response
+
--- a/foodtruck/web.py	Sun Jan 24 10:42:33 2016 -0800
+++ b/foodtruck/web.py	Wed Jan 27 18:02:25 2016 -0800
@@ -22,7 +22,7 @@
     l = logging.getLogger()
     l.setLevel(logging.DEBUG)
 
-app.logger.debug("Using FoodTruck admin root: %s" % admin_root)
+logger.debug("Using FoodTruck admin root: %s" % admin_root)
 
 
 def after_this_request(f):
@@ -73,7 +73,7 @@
 
 
 if not app.config['DEBUG']:
-    app.logger.debug("Registering exception handlers.")
+    logger.debug("Registering exception handlers.")
 
     @app.errorhandler(FoodTruckConfigNotFoundError)
     def _on_config_missing(ex):
@@ -81,7 +81,8 @@
 
     @app.errorhandler(InvalidSiteError)
     def _on_invalid_site(ex):
-        data = {'error': 'invalid_site', 'exception': str(ex)}
+        data = {'error': "The was an error with your configuration file: %s" %
+                str(ex)}
         return render_template('error.html', **data)
 
 
@@ -139,12 +140,12 @@
 app.bcrypt = Bcrypt(app)
 
 
-import foodtruck.views.baking  # NOQA
 import foodtruck.views.create  # NOQA
 import foodtruck.views.dashboard  # NOQA
 import foodtruck.views.edit  # NOQA
 import foodtruck.views.menu  # NOQA
 import foodtruck.views.preview  # NOQA
+import foodtruck.views.publish  # NOQA
 import foodtruck.views.settings  # NOQA
 import foodtruck.views.sources  # NOQA
 
--- a/foodtruckui/assets/js/foodtruck.js	Sun Jan 24 10:42:33 2016 -0800
+++ b/foodtruckui/assets/js/foodtruck.js	Wed Jan 27 18:02:25 2016 -0800
@@ -3,41 +3,41 @@
     $('.ft-nav-collapsed + ul').hide();
 });
 
+var onPublishEvent = function(e) {
+    var msgEl = $('<div></div>');
+
+    var removeMsgEl = function() {
+        msgEl.remove();
+        var publogEl = $('#ft-publog');
+        if (publogEl.children().length == 0) {
+            publogEl.hide();
+        }
+    };
+
+    msgEl.addClass('alert-dismissible');
+    msgEl.attr('role', 'alert');
+    msgEl.append('<button type="button" class="close" data-dismiss="alert" aria-label="close">' +
+                 '<span aria-hidden="true">&times;</span></button>');
+    msgEl.append('<div>' + e.data + '</div>');
+    var timeoutId = window.setTimeout(function() {
+        msgEl.fadeOut(400, removeMsgEl);
+    }, 4000);
+    msgEl.mouseenter(function() {
+        window.clearTimeout(timeoutId);
+    });
+    $('button', msgEl).click(removeMsgEl);
+
+    var logEl = $('#ft-publog');
+    logEl.append(msgEl);
+    logEl.show();
+};
+
 if (!!window.EventSource) {
-    var source = new EventSource('/bakelog');
-
+    var source = new EventSource('/publish-log');
     source.onerror = function(e) {
         console.log("Error with SSE, closing.", e);
         source.close();
     };
-    source.addEventListener('message', function(e) {
-        var msgEl = $('<div></div>');
-
-        var removeMsgEl = function() {
-            msgEl.remove();
-            var bakelogEl = $('#ft-bakelog');
-            if (bakelogEl.children().length == 0) {
-                bakelogEl.hide();
-            }
-        };
-
-        msgEl.addClass('alert-dismissible');
-        msgEl.attr('role', 'alert');
-        msgEl.append('<button type="button" class="close" data-dismiss="alert" aria-label="close">' +
-                     '<span aria-hidden="true">&times;</span></button>');
-        msgEl.append('<p>' + e.data + '</p>');
-        var timeoutId = window.setTimeout(function() {
-            msgEl.fadeOut(400, removeMsgEl);
-        }, 4000);
-        msgEl.mouseenter(function() {
-            window.clearTimeout(timeoutId);
-        });
-        $('button', msgEl).click(removeMsgEl);
-
-        var logEl = $('#ft-bakelog');
-        logEl.append(msgEl);
-        logEl.show();
-    });
+    source.addEventListener('message', onPublishEvent);
 }
 
-
--- a/foodtruckui/assets/sass/foodtruck/_base.scss	Sun Jan 24 10:42:33 2016 -0800
+++ b/foodtruckui/assets/sass/foodtruck/_base.scss	Wed Jan 27 18:02:25 2016 -0800
@@ -45,18 +45,21 @@
     padding: 0 0.2em;
 }
 
-#ft-bakelog {
+#ft-publog {
     position: fixed;
     right: 0;
     bottom: 0;
     margin: 0.5em;
-    padding: 1em;
     width: 50%;
     max-width: 30em;
-    color: $ft-color-gray;
-    background: $ft-color-gray-lighter;
-    border: 1px solid $ft-color-gray-light;
-    border-radius: 0.5em;
     display: none;
 }
+#ft-publog>div {
+    padding: 1em 35px 1em 1em;
+    margin: 0.3em;
+    color: $ft-color-gray-light;
+    background: $ft-color-gray-darker;
+    border-radius: 0.5em;
+    box-shadow: 0 0 10px $ft-color-gray-dark;
+}