diff --git a/nodejs/package.json.in b/nodejs/package.json.in
index 9d546cb..d9fbcc9 100644
--- a/nodejs/package.json.in
+++ b/nodejs/package.json.in
@@ -3,10 +3,11 @@
   "version": "@PACKAGE_VERSION@",
   "private": true,
   "dependencies": {
-    "express": "~2.5.8",
-    "stylus": "~0.53.0",
-    "ejs": ">= 0.0.1",
-    "socket.io": "~1.4.4"
+      "express": "~2.5.8",
+      "stylus": "~0.53.0",
+      "ejs": ">= 0.0.1",
+      "socket.io": "~1.4.4",
+      "pty.js": "~0.3.0"
   },
   "description": "Docker as a Service",
   "main": "servicedock.js",
diff --git a/nodejs/public/javascripts/servicedock.js b/nodejs/public/javascripts/servicedock.js
index ad5f0cb..1c6b707 100644
--- a/nodejs/public/javascripts/servicedock.js
+++ b/nodejs/public/javascripts/servicedock.js
@@ -13,11 +13,11 @@ var focused = null;
 
 function DockerContainers() {
     var Status = Object.freeze({
-        Error:      {color: "red", action1: "start", action2: "remove"},
-        Terminated: {color: "yellow", action1: "start", action2: "remove"},
-        Restarting: {color: "lightblue", action1: "start", action2: "remove"},
-        Paused:     {color: "lightgrey", action1: "unpause", action2: null},
-        Running:    {color: "lightgreen", action1: "pause", action2: "stop"}
+        Error:      {color: "red", action1: "start", action2: "remove", bash: false},
+        Terminated: {color: "yellow", action1: "start", action2: "remove", bash: false},
+        Restarting: {color: "lightblue", action1: "start", action2: "remove", bash: false},
+        Paused:     {color: "lightgrey", action1: "unpause", action2: null, bash: false},
+        Running:    {color: "lightgreen", action1: "pause", action2: "stop", bash: true}
     });
     var containers = [];
     var nodes = [];
@@ -40,7 +40,7 @@ function DockerContainers() {
             if (n.status.action1) {
                 $("#popup").append('');
                 $("#popup1").click(function() {
-                    socket.emit(n.status.action1, name);
+                    emit(n.status.action1, name);
                 });
             }
             $("#popup").append('');
@@ -50,11 +50,27 @@ function DockerContainers() {
             if (n.status.action2) {
                 $("#popup").append('');
                 $("#popup3").click(function() {
-                    socket.emit(n.status.action2, name);
+                    emit(n.status.action2, name);
             });
             }
             $("#popup").append('
');
-            $("#popup").append('');
+            $("#popup").append('');
+            $("#popup4").click(function() {
+                emit("logs", name);
+            });
+            if (n.status.bash) {
+                $("#popup").append('');
+                $("#popup5").click(function() {
+                    emit("bash-start", name);
+                    $("#console").show();
+                    $("#main").hide();
+                    $("#bash").submit(function() {
+                        emit("bash-input", {name: name, text: $("#command").val()+"\n"});
+                        $("#command").val("");
+                    })
+                });
+            }
+            $("#popup").append('');
             $("#popup").css("position", "fixed");
             $("#popup").css("top", e.pageY-$("#popup").height()/4);
             $("#popup").css("left", e.pageX-$("#popup").width()/2);
@@ -66,49 +82,6 @@ function DockerContainers() {
             $("#popup").show();
         })
     }
-    this.details = function(name) {
-        var res = `
-                
| Name | -Ports | -Volumes | -Links | -Environments | -Image | -Command | -
|---|
`; - res += JSON.stringify(containers[nodes[name].id], null, 4); - res += ` --
"+e+"
"+res.join("\n")+"");
-        }
-    }}).fail(function() {
-        error("offline");
-    });
-}
-
 function containers(c) {
     console.log("->rcv containers");
     dc.setContainers(c);
@@ -550,6 +493,109 @@ function containers(c) {
         overview();
 }
 
+function logs(data) {
+    console.log("->rcv logs("+data.name+")");
+    $("#main").hide();
+    $("#console").hide();
+    $("#logs").show();
+    if (data.type=='done') {
+        $("#logs").append('\nDONE');
+    } else {
+        $("#logs").append(''+htmlenc(data.text)+'');
+    }
+}
+
+function strInsert(str, pos, txt) {
+    return str.slice(0, pos)+txt+str.slice(pos);
+}
+
+function ansifilter(data) {
+    console.log("ansifilter");
+    var res = data;
+    var pos = -1;
+    var spans = 0;
+    while ((pos=res.indexOf("\x1B[")) >= 0) {
+        var end = res.indexOf("m", pos+2);
+        if (end>0) {
+            var control= res.slice(pos+2, end);
+            res = res.slice(0, pos)+res.slice(end+1);
+            control.split(';').forEach(function(c) {
+                switch (parseInt(c)) {
+                    // set
+                    case 1: res = strInsert(res, pos, ''); ++spans; break;
+                    case 2: res = strInsert(res, pos, ''); ++spans; break;
+                    case 4: res = strInsert(res, pos, ''); ++spans; break;
+                    case 5: res = strInsert(res, pos, ''); ++spans; break;
+                    case 7: res = strInsert(res, pos, ''); ++spans; break;
+                    case 8: res = strInsert(res, pos, ''); ++spans; break;
+                    // reset
+                    case 0: for (;spans;--spans) res = strInsert(res, pos, ""); break;
+                    case 21: if (spans) --spans; res = strInsert(res, pos, ""); break;
+                    case 22: if (spans) --spans; res = strInsert(res, pos, ""); break;
+                    case 23: if (spans) --spans; res = strInsert(res, pos, ""); break;
+                    case 25: if (spans) --spans; res = strInsert(res, pos, ""); break;
+                    case 27: if (spans) --spans; res = strInsert(res, pos, ""); break;
+                    case 28: if (spans) --spans; res = strInsert(res, pos, ""); break;
+                    // fg colors
+                    case 39: res = strInsert(res, pos, ''); ++spans; break;
+                    case 30: res = strInsert(res, pos, ''); ++spans; break;
+                    case 31: res = strInsert(res, pos, ''); ++spans; break;
+                    case 32: res = strInsert(res, pos, ''); ++spans; break;
+                    case 33: res = strInsert(res, pos, ''); ++spans; break;
+                    case 34: res = strInsert(res, pos, ''); ++spans; break;
+                    case 35: res = strInsert(res, pos, ''); ++spans; break;
+                    case 36: res = strInsert(res, pos, ''); ++spans; break;
+                    case 37: res = strInsert(res, pos, ''); ++spans; break;
+                    case 90: res = strInsert(res, pos, ''); ++spans; break;
+                    case 91: res = strInsert(res, pos, ''); ++spans; break;
+                    case 92: res = strInsert(res, pos, ''); ++spans; break;
+                    case 93: res = strInsert(res, pos, ''); ++spans; break;
+                    case 94: res = strInsert(res, pos, ''); ++spans; break;
+                    case 95: res = strInsert(res, pos, ''); ++spans; break;
+                    case 96: res = strInsert(res, pos, ''); ++spans; break;
+                    case 97: res = strInsert(res, pos, ''); ++spans; break;
+                    // bg colors
+                    case 49: res = strInsert(res, pos, ''); ++spans; break;
+                    case 40: res = strInsert(res, pos, ''); ++spans; break;
+                    case 41: res = strInsert(res, pos, ''); ++spans; break;
+                    case 42: res = strInsert(res, pos, ''); ++spans; break;
+                    case 43: res = strInsert(res, pos, ''); ++spans; break;
+                    case 44: res = strInsert(res, pos, ''); ++spans; break;
+                    case 45: res = strInsert(res, pos, ''); ++spans; break;
+                    case 46: res = strInsert(res, pos, ''); ++spans; break;
+                    case 47: res = strInsert(res, pos, ''); ++spans; break;
+                    case 100: res = strInsert(res, pos, ''); ++spans; break;
+                    case 101: res = strInsert(res, pos, ''); ++spans; break;
+                    case 102: res = strInsert(res, pos, ''); ++spans; break;
+                    case 103: res = strInsert(res, pos, ''); ++spans; break;
+                    case 104: res = strInsert(res, pos, ''); ++spans; break;
+                    case 105: res = strInsert(res, pos, ''); ++spans; break;
+                    case 106: res = strInsert(res, pos, ''); ++spans; break;
+                    case 107: res = strInsert(res, pos, ''); ++spans; break;
+                    
+                }
+            });
+        } else {
+            break;
+        }
+    }
+    for (;spans;--spans) res += "";
+    console.log(res);
+    return res.replace(/\r\r\n/g, '\n');
+}
+
+function bash_data(data) {
+    console.log("->rcv bash-data("+data.name+")", data);
+    $("#main").hide();
+    $("#logs").hide();
+    $("#console").show();
+    if (data.type=='done') {
+        $("#screen").append('\nDONE');
+    } else {
+        $("#screen").append(''+ansifilter(htmlenc(data.text))+'');
+    }
+}
+
 function overview() {
     focused = null;
     showviz(dc.graph());
@@ -577,7 +623,9 @@ function init() {
         .on("error", disconnected);
     socket
         .on("fail", error)
-        .on("containers", containers);
+        .on("containers", containers)
+        .on("logs", logs)
+        .on("bash-data", bash_data);
     start();
 }
 
diff --git a/nodejs/public/stylesheets/servicedock.css b/nodejs/public/stylesheets/servicedock.css
index d126f0e..2aff43a 100644
--- a/nodejs/public/stylesheets/servicedock.css
+++ b/nodejs/public/stylesheets/servicedock.css
@@ -3,6 +3,10 @@
   padding: 0;
 }
 
+body {
+  background-color: blue;
+}
+
 @media (min-resolution: 120dpi) {
   html {
     font-size: 120%;
@@ -228,18 +232,22 @@ table.docker li+li {
   padding-top: 0.5em;
 }
 
-#main {
+#main, #logs, #console {
   position: fixed;
-  top: 2em;
+  top: 1.5em;
   left: 0;
   right: 0;
-  bottom: 2em;
-  padding: 0em 1em 0em 1em;
+  bottom: 1.5em;
+  padding: 1em 1em 1em 1em;
   clear: both;
   overflow: auto;
   z-index: 0;
 }
 
+#main {
+  background-color: white;
+}
+
 #popup {
   position: fixed;
   background-color: lightblue;
@@ -308,3 +316,62 @@ table.docker li+li {
 #preview img {
   height: 4em;
 }
+
+#logs {
+  background-color: lightgray;
+}
+#logs .stdout {
+  color: black;
+}
+.stderr {
+  color: red;
+}
+.done {
+  color: green;
+}
+
+#console {
+  background-color: black;
+  color: white
+}
+.bold {font-weight: bold}
+.dim {color: grey}
+.underline {text-decoration: underline}
+.blink {animation: blinker 1s linear infinite}
+@keyframes blinker {50% { opacity: 0.0;}}
+.reverse {color: black; background-color: white}
+.hidden {color: black}
+.fgdefault {color: white}
+.fgblack {color: black}
+.fgred {color: red}
+.fggreen {color: green}
+.fgyellow {color: yellow}
+.fgblue {color: blue}
+.fgmagenta {color: magenta}
+.fgcyan {color: cyan}
+.fglightgrey {color: lightgrey}
+.fgdarkgrey {color: darkgrey}
+.fglightred {color: lightred}
+.fglightgreen {color: lightgreen}
+.fglightyellow {color: lightyellow}
+.fglightblue {color: lightblue}
+.fglightmagenta {color: lightmagenta}
+.fglightcyan {color: lightcyan}
+.fgwhite {color: white}
+.bgdefault {background-color: black}
+.bgblack {background-color: black}
+.bgred {background-color: red}
+.bggreen {background-color: green}
+.bgyellow {background-color: yellow}
+.bgblue {background-color: blue}
+.bgmagenta {background-color: magenta}
+.bgcyan {background-color: cyan}
+.bglightgrey {background-color: lightgrey}
+.bgdarkgrey {background-color: darkgrey}
+.bglightred {background-color: lightred}
+.bglightgreen {background-color: lightgreen}
+.bglightyellow {background-color: lightyellow}
+.bglightblue {background-color: lightblue}
+.bglightmagenta {background-color: lightmagenta}
+.bglightcyan {background-color: lightcyan}
+.bgwhite {background-color: white}
diff --git a/nodejs/sockets/index.js b/nodejs/sockets/index.js
index a5670e3..b6c4c19 100644
--- a/nodejs/sockets/index.js
+++ b/nodejs/sockets/index.js
@@ -5,6 +5,7 @@ module.exports = function() {
     module.connection = function(socket) {
 
         //var sys = require('sys');
+        var pty = require('pty.js');
         var proc = require('child_process');
 
         console.log("new client");
@@ -60,9 +61,7 @@ module.exports = function() {
 
         function modify(cmd, name) {
             if (!name.match(/^[a-z0-9][-_:.+a-z0-9]*$/i))
-                return fail("illegal instance name", {
-                    error: error, stderr: stderr, stdout: stdout
-                });
+                return fail("illegal instance name");
             exec("docker "+cmd+" "+name, updatecontainers);
         }
 
@@ -96,14 +95,62 @@ module.exports = function() {
             modify("rm", name);
         }
 
+        function logs(name) {
+            console.log("-> logs("+name+")");
+            var l = proc.spawn("docker", ["logs", "-f", name])
+                .on('close', function(code) {
+                    emit('logs', {name: name, type: 'done'});
+                });
+            l.stdout.on('data', function(data) {
+                emit('logs', {name: name, type: 'stdout', text: data.toString()});
+            });
+            l.stderr.on('data', function(data) {
+                emit('logs', {name: name, type: 'stderr', text: data.toString()});
+            });
+        }
+
+        var bash_connections = {};
+
+        function bash_start(name) {
+            console.log("-> bash-start("+name+")");
+            if (!name.match(/^[a-z0-9][-_:.+a-z0-9]*$/i))
+                return fail("illegal instance name");
+            if (bash_connections[name]) return fail("bash already open");
+            bash_connections[name] =
+                pty.spawn("docker", ["exec", "-it", name, "bash", "-i"]);
+            bash_connections[name].stdout.on('data', function(data) {
+                emit('bash-data', {name: name, type: 'stdout', text: data.toString()});
+            });
+            // bash_connections[name].stderr.on('data', function(data) {
+            //     emit('bash-data', {name: name, type: 'stdout', text: data.toString()});
+            // });
+        }
+        
+        function bash_input(data) {
+            console.log("-> bash-input("+data.name+", "+data.text+")");
+            if (!bash_connections[data.name]) return fail("bash not open");
+            bash_connections[data.name].stdin.resume();
+            bash_connections[data.name].stdin.write(data.text);
+        }
+
+        // function bash_end(name, text) {
+        //     console.log("-> bash-end("+name+")");
+        //     if (!bash_connections[name]) return fail("bash not open");
+        //     bash_connections[name].stdin.close();
+        // }
+
         socket
             .on("containers", containers)
             .on("start", start)
             .on("stop", stop)
             .on("pause", pause)
             .on("unpause", unpause)
-            .on("remove", remove);
-                
+            .on("remove", remove)
+            .on('logs', logs)
+            .on('bash-start', bash_start)
+            .on('bash-input', bash_input);
+            //.on('bash-end', bash_end);
+        
     }
     
     return module;
diff --git a/nodejs/views/index.ejs b/nodejs/views/index.ejs
index 5934c35..7e38332 100644
--- a/nodejs/views/index.ejs
+++ b/nodejs/views/index.ejs
@@ -44,6 +44,16 @@
       
     
 
+    
+
+    
+