Merge branch 'master' of ssh://gerrit.onlab.us:29418/onos-next
diff --git a/core/net/src/main/java/org/onlab/onos/net/flow/impl/FlowRuleManager.java b/core/net/src/main/java/org/onlab/onos/net/flow/impl/FlowRuleManager.java
index bd7fb94..e996dfc 100644
--- a/core/net/src/main/java/org/onlab/onos/net/flow/impl/FlowRuleManager.java
+++ b/core/net/src/main/java/org/onlab/onos/net/flow/impl/FlowRuleManager.java
@@ -296,7 +296,7 @@
                     post(event);
                 }
             } else {
-                log.info("Removing flow rules....");
+                log.debug("Removing flow rules....");
                 removeFlowRules(flowEntry);
             }
 
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/MapDBLog.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/MapDBLog.java
index 663e0f0..637a643 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/MapDBLog.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/MapDBLog.java
@@ -2,13 +2,16 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Verify.verifyNotNull;
 import static org.slf4j.LoggerFactory.getLogger;
 
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ConcurrentNavigableMap;
 
 import net.kuujo.copycat.log.Entry;
@@ -25,8 +28,6 @@
 import org.onlab.onos.store.serializers.StoreSerializer;
 import org.slf4j.Logger;
 
-import com.google.common.collect.Lists;
-
 /**
  * MapDB based log implementation.
  */
@@ -84,7 +85,7 @@
     public List<Long> appendEntries(List<Entry> entries) {
         assertIsOpen();
         checkArgument(entries != null, "expecting non-null entries");
-        final List<Long> indices = Lists.newArrayList();
+        final List<Long> indices = new ArrayList<>(entries.size());
 
         txMaker.execute(new TxBlock() {
             @Override
@@ -92,13 +93,15 @@
                 BTreeMap<Long, byte[]> log = getLogMap(db);
                 Atomic.Long size = db.getAtomicLong(SIZE_FIELD_NAME);
                 long nextIndex = log.isEmpty() ? 1 : log.lastKey() + 1;
+                long addedBytes = 0;
                 for (Entry entry : entries) {
                     byte[] entryBytes = serializer.encode(entry);
                     log.put(nextIndex, entryBytes);
-                    size.addAndGet(entryBytes.length);
+                    addedBytes += entryBytes.length;
                     indices.add(nextIndex);
                     nextIndex++;
                 }
+                size.addAndGet(addedBytes);
             }
         });
 
@@ -236,12 +239,15 @@
             public void tx(DB db) {
                 BTreeMap<Long, byte[]> log = getLogMap(db);
                 Atomic.Long size = db.getAtomicLong(SIZE_FIELD_NAME);
-                long startIndex = index + 1;
-                long endIndex = log.lastKey();
-                for (long i = startIndex; i <= endIndex; ++i) {
-                    byte[] entryBytes = log.remove(i);
-                    size.addAndGet(-1L * entryBytes.length);
+                long removedBytes = 0;
+                ConcurrentNavigableMap<Long, byte[]> tailMap = log.tailMap(index, false);
+                Iterator<Map.Entry<Long, byte[]>> it = tailMap.entrySet().iterator();
+                while (it.hasNext()) {
+                    Map.Entry<Long, byte[]> entry = it.next();
+                    removedBytes += entry.getValue().length;
+                    it.remove();
                 }
+                size.addAndGet(-removedBytes);
             }
         });
     }
@@ -273,9 +279,16 @@
                 BTreeMap<Long, byte[]> log = getLogMap(db);
                 Atomic.Long size = db.getAtomicLong(SIZE_FIELD_NAME);
                 ConcurrentNavigableMap<Long, byte[]> headMap = log.headMap(index);
-                long deletedBytes = headMap.keySet().stream().mapToLong(i -> log.remove(i).length).sum();
-                size.addAndGet(-1 * deletedBytes);
-                byte[] entryBytes = serializer.encode(entry);
+                Iterator<Map.Entry<Long, byte[]>> it = headMap.entrySet().iterator();
+
+                long deletedBytes = 0;
+                while (it.hasNext()) {
+                    Map.Entry<Long, byte[]> e = it.next();
+                    deletedBytes += e.getValue().length;
+                    it.remove();
+                }
+                size.addAndGet(-deletedBytes);
+                byte[] entryBytes = verifyNotNull(serializer.encode(entry));
                 byte[] existingEntry = log.put(index, entryBytes);
                 if (existingEntry != null) {
                     size.addAndGet(entryBytes.length - existingEntry.length);
diff --git a/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilderVer13.java b/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilderVer13.java
index 88dfd1c..9ed0f58 100644
--- a/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilderVer13.java
+++ b/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilderVer13.java
@@ -139,7 +139,7 @@
                 .setXid(cookie)
                 .setCookie(U64.of(cookie))
                 .setBufferId(OFBufferId.NO_BUFFER)
-                .setActions(actions)
+                //.setActions(actions) //FIXME do we want to send actions in flowdel?
                 //.setInstructions(Collections.singletonList(writeActions))
                 .setMatch(match)
                 .setFlags(Collections.singleton(OFFlowModFlags.SEND_FLOW_REM))
diff --git a/tools/dev/bash_profile b/tools/dev/bash_profile
index 7b21916..c383c54 100644
--- a/tools/dev/bash_profile
+++ b/tools/dev/bash_profile
@@ -19,13 +19,13 @@
 export MAVEN=${MAVEN:-~/Applications/apache-maven-3.2.2}
 
 export KARAF_VERSION=${KARAF_VERSION:-3.0.2}
-export KARAF=${KARAF:-~/Applications/apache-karaf-$KARAF_VERSION}
-export KARAF_LOG=$KARAF/data/log/karaf.log
+export KARAF_HOME=${KARAF:-~/Applications/apache-karaf-$KARAF_VERSION}
+export KARAF_LOG=$KARAF_HOME/data/log/karaf.log
 
 # Setup a path
 export PATH="$PATH:$ONOS_ROOT/tools/dev/bin:$ONOS_ROOT/tools/test/bin"
 export PATH="$PATH:$ONOS_ROOT/tools/build"
-export PATH="$PATH:$MAVEN/bin:$KARAF/bin"
+export PATH="$PATH:$MAVEN/bin:$KARAF_HOME/bin"
 
 # Convenience utility to warp to various ONOS source projects
 # e.g. 'o api', 'o dev', 'o'
diff --git a/tools/dev/onos.cshrc b/tools/dev/onos.cshrc
index 17d1f06..f3dc2e6 100644
--- a/tools/dev/onos.cshrc
+++ b/tools/dev/onos.cshrc
@@ -29,8 +29,8 @@
 if ( ! $?KARAF_VERSION ) then
     setenv KARAF_VERSION 3.0.2
 endif
-if ( ! $?KARAF ) then
-    setenv KARAF $HOME/Applications/apache-karaf-$KARAF_VERSION
+if ( ! $?KARAF_HOME ) then
+    setenv KARAF_HOME $HOME/Applications/apache-karaf-$KARAF_VERSION
 endif
 setenv KARAF_LOG $KARAF/data/log/karaf.log
 
diff --git a/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java b/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java
index 31c3f84..f433691 100644
--- a/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java
+++ b/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java
@@ -20,50 +20,95 @@
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.eclipse.jetty.websocket.WebSocket;
-import org.onlab.onos.event.Event;
+import org.onlab.onos.core.ApplicationId;
+import org.onlab.onos.core.CoreService;
+import org.onlab.onos.mastership.MastershipEvent;
+import org.onlab.onos.mastership.MastershipListener;
+import org.onlab.onos.mastership.MastershipService;
 import org.onlab.onos.net.Annotations;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DefaultEdgeLink;
 import org.onlab.onos.net.Device;
 import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.ElementId;
+import org.onlab.onos.net.Host;
+import org.onlab.onos.net.HostId;
+import org.onlab.onos.net.HostLocation;
 import org.onlab.onos.net.Link;
 import org.onlab.onos.net.Path;
 import org.onlab.onos.net.device.DeviceEvent;
+import org.onlab.onos.net.device.DeviceListener;
 import org.onlab.onos.net.device.DeviceService;
+import org.onlab.onos.net.flow.DefaultTrafficSelector;
+import org.onlab.onos.net.flow.DefaultTrafficTreatment;
+import org.onlab.onos.net.host.HostEvent;
+import org.onlab.onos.net.host.HostListener;
+import org.onlab.onos.net.host.HostService;
+import org.onlab.onos.net.intent.HostToHostIntent;
+import org.onlab.onos.net.intent.Intent;
+import org.onlab.onos.net.intent.IntentEvent;
+import org.onlab.onos.net.intent.IntentId;
+import org.onlab.onos.net.intent.IntentListener;
+import org.onlab.onos.net.intent.IntentService;
+import org.onlab.onos.net.intent.PathIntent;
 import org.onlab.onos.net.link.LinkEvent;
-import org.onlab.onos.net.topology.Topology;
-import org.onlab.onos.net.topology.TopologyEdge;
-import org.onlab.onos.net.topology.TopologyEvent;
-import org.onlab.onos.net.topology.TopologyGraph;
-import org.onlab.onos.net.topology.TopologyListener;
-import org.onlab.onos.net.topology.TopologyService;
-import org.onlab.onos.net.topology.TopologyVertex;
+import org.onlab.onos.net.link.LinkListener;
+import org.onlab.onos.net.link.LinkService;
+import org.onlab.onos.net.provider.ProviderId;
+import org.onlab.onos.net.topology.PathService;
 import org.onlab.osgi.ServiceDirectory;
+import org.onlab.packet.IpAddress;
 
 import java.io.IOException;
-import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static org.onlab.onos.net.DeviceId.deviceId;
+import static org.onlab.onos.net.HostId.hostId;
+import static org.onlab.onos.net.PortNumber.portNumber;
 import static org.onlab.onos.net.device.DeviceEvent.Type.DEVICE_ADDED;
 import static org.onlab.onos.net.device.DeviceEvent.Type.DEVICE_REMOVED;
+import static org.onlab.onos.net.host.HostEvent.Type.HOST_ADDED;
+import static org.onlab.onos.net.host.HostEvent.Type.HOST_REMOVED;
 import static org.onlab.onos.net.link.LinkEvent.Type.LINK_ADDED;
 import static org.onlab.onos.net.link.LinkEvent.Type.LINK_REMOVED;
 
 /**
  * Web socket capable of interacting with the GUI topology view.
  */
-public class TopologyWebSocket implements WebSocket.OnTextMessage, TopologyListener {
+public class TopologyWebSocket implements WebSocket.OnTextMessage {
 
+    private static final String APP_ID = "org.onlab.onos.gui";
+    private static final ProviderId PID = new ProviderId("core", "org.onlab.onos.core", true);
+
+    private final ApplicationId appId;
     private final ServiceDirectory directory;
-    private final TopologyService topologyService;
-    private final DeviceService deviceService;
 
     private final ObjectMapper mapper = new ObjectMapper();
 
     private Connection connection;
 
+    private final DeviceService deviceService;
+    private final LinkService linkService;
+    private final HostService hostService;
+    private final MastershipService mastershipService;
+    private final IntentService intentService;
+
+    private final DeviceListener deviceListener = new InternalDeviceListener();
+    private final LinkListener linkListener = new InternalLinkListener();
+    private final HostListener hostListener = new InternalHostListener();
+    private final MastershipListener mastershipListener = new InternalMastershipListener();
+    private final IntentListener intentListener = new InternalIntentListener();
+
     // TODO: extract into an external & durable state; good enough for now and demo
-    private static Map<String, ObjectNode> metaUi = new HashMap<>();
+    private static Map<String, ObjectNode> metaUi = new ConcurrentHashMap<>();
+
+    // Intents that are being monitored for the GUI
+    private static Map<IntentId, Long> intentsToMonitor = new ConcurrentHashMap<>();
 
     private static final String COMPACT = "%s/%s-%s/%s";
 
@@ -74,145 +119,127 @@
      * @param directory service directory
      */
     public TopologyWebSocket(ServiceDirectory directory) {
-        this.directory = directory;
-        topologyService = directory.get(TopologyService.class);
+        this.directory = checkNotNull(directory, "Directory cannot be null");
         deviceService = directory.get(DeviceService.class);
+        linkService = directory.get(LinkService.class);
+        hostService = directory.get(HostService.class);
+        mastershipService = directory.get(MastershipService.class);
+        intentService = directory.get(IntentService.class);
+
+        appId = directory.get(CoreService.class).registerApplication(APP_ID);
     }
 
     @Override
     public void onOpen(Connection connection) {
         this.connection = connection;
+        deviceService.addListener(deviceListener);
+        linkService.addListener(linkListener);
+        hostService.addListener(hostListener);
+        mastershipService.addListener(mastershipListener);
+        intentService.addListener(intentListener);
 
-        // Register for topology events...
-        if (topologyService != null && deviceService != null) {
-            topologyService.addListener(this);
+        sendAllDevices();
+        sendAllLinks();
+        sendAllHosts();
+    }
 
-            Topology topology = topologyService.currentTopology();
-            TopologyGraph graph = topologyService.getGraph(topology);
-            for (TopologyVertex vertex : graph.getVertexes()) {
-                sendMessage(message(new DeviceEvent(DEVICE_ADDED,
-                                                    deviceService.getDevice(vertex.deviceId()))));
-            }
+    private void sendAllHosts() {
+        for (Host host : hostService.getHosts()) {
+            sendMessage(hostMessage(new HostEvent(HOST_ADDED, host)));
+        }
+    }
 
-            for (TopologyEdge edge : graph.getEdges()) {
-                sendMessage(message(new LinkEvent(LINK_ADDED, edge.link())));
-            }
+    private void sendAllDevices() {
+        for (Device device : deviceService.getDevices()) {
+            sendMessage(deviceMessage(new DeviceEvent(DEVICE_ADDED, device)));
+        }
+    }
 
-        } else {
-            sendMessage(message("error", "No topology service!!!"));
+    private void sendAllLinks() {
+        for (Link link : linkService.getLinks()) {
+            sendMessage(linkMessage(new LinkEvent(LINK_ADDED, link)));
         }
     }
 
     @Override
     public void onClose(int closeCode, String message) {
-        TopologyService topologyService = directory.get(TopologyService.class);
-        if (topologyService != null) {
-            topologyService.removeListener(this);
-        }
+        deviceService.removeListener(deviceListener);
+        linkService.removeListener(linkListener);
+        hostService.removeListener(hostListener);
+        mastershipService.removeListener(mastershipListener);
     }
 
     @Override
     public void onMessage(String data) {
         try {
             ObjectNode event = (ObjectNode) mapper.reader().readTree(data);
-            String type = event.path("event").asText("unknown");
-            ObjectNode payload = (ObjectNode) event.path("payload");
-
-            switch (type) {
-                case "updateMeta":
-                    metaUi.put(payload.path("id").asText(), payload);
-                    break;
-                case "requestPath":
-                    findPath(deviceId(payload.path("one").asText()),
-                             deviceId(payload.path("two").asText()));
-                default:
-                    break;
+            String type = string(event, "event", "unknown");
+            if (type.equals("showDetails")) {
+                showDetails(event);
+            } else if (type.equals("updateMeta")) {
+                updateMetaInformation(event);
+            } else if (type.equals("requestPath")) {
+                createHostIntent(event);
+            } else if (type.equals("requestTraffic")) {
+                sendTraffic(event);
+            } else if (type.equals("cancelTraffic")) {
+                cancelTraffic(event);
             }
-        } catch (IOException e) {
-            System.out.println("Received: " + data);
+        } catch (Exception e) {
+            System.out.println("WTF?! " + data);
+            e.printStackTrace();
         }
     }
 
-    private void findPath(DeviceId one, DeviceId two) {
-        Set<Path> paths = topologyService.getPaths(topologyService.currentTopology(),
-                                                   one, two);
-        if (!paths.isEmpty()) {
-            ObjectNode payload = mapper.createObjectNode();
-            ArrayNode links = mapper.createArrayNode();
-
-            Path path = paths.iterator().next();
-            for (Link link : path.links()) {
-                links.add(compactLinkString(link));
-            }
-
-            payload.set("links", links);
-            sendMessage(envelope("showPath", payload));
-        }
-        // TODO: when no path, send a message to the client
-    }
-
-    /**
-     * Returns a compact string representing the given link.
-     *
-     * @param link infrastructure link
-     * @return formatted link string
-     */
-    public static String compactLinkString(Link link) {
-        return String.format(COMPACT, link.src().deviceId(), link.src().port(),
-                             link.dst().deviceId(), link.dst().port());
-    }
-
-
-    private void sendMessage(String data) {
+    // Sends the specified data to the client.
+    private void sendMessage(ObjectNode data) {
         try {
-            connection.sendMessage(data);
+            connection.sendMessage(data.toString());
         } catch (IOException e) {
             e.printStackTrace();
         }
     }
 
-    // Produces a link event message to the client.
-    private String message(DeviceEvent event) {
-        Device device = event.subject();
-        ObjectNode payload = mapper.createObjectNode()
-                .put("id", device.id().toString())
-                .put("type", device.type().toString().toLowerCase())
-                .put("online", deviceService.isAvailable(device.id()));
-
-        // Generate labels: id, chassis id, no-label, optional-name
-        ArrayNode labels = mapper.createArrayNode();
-        labels.add(device.id().toString());
-        labels.add(device.chassisId().toString());
-        labels.add(" "); // compact no-label view
-        labels.add(device.annotations().value("name"));
-
-        // Add labels, props and stuff the payload into envelope.
-        payload.set("labels", labels);
-        payload.set("props", props(device.annotations()));
-
-        ObjectNode meta = metaUi.get(device.id().toString());
-        if (meta != null) {
-            payload.set("metaUi", meta);
-        }
-
-        String type = (event.type() == DEVICE_ADDED) ? "addDevice" :
-                ((event.type() == DEVICE_REMOVED) ? "removeDevice" : "updateDevice");
-        return envelope(type, payload);
+    // Retrieves the payload from the specified event.
+    private ObjectNode payload(ObjectNode event) {
+        return (ObjectNode) event.path("payload");
     }
 
-    // Produces a link event message to the client.
-    private String message(LinkEvent event) {
-        Link link = event.subject();
-        ObjectNode payload = mapper.createObjectNode()
-                .put("type", link.type().toString().toLowerCase())
-                .put("linkWidth", 2)
-                .put("src", link.src().deviceId().toString())
-                .put("srcPort", link.src().port().toString())
-                .put("dst", link.dst().deviceId().toString())
-                .put("dstPort", link.dst().port().toString());
-        String type = (event.type() == LINK_ADDED) ? "addLink" :
-                ((event.type() == LINK_REMOVED) ? "removeLink" : "removeLink");
-        return envelope(type, payload);
+    // Returns the specified node property as a number
+    private long number(ObjectNode node, String name) {
+        return node.path(name).asLong();
+    }
+
+    // Returns the specified node property as a string.
+    private String string(ObjectNode node, String name) {
+        return node.path(name).asText();
+    }
+
+    // Returns the specified node property as a string.
+    private String string(ObjectNode node, String name, String defaultValue) {
+        return node.path(name).asText(defaultValue);
+    }
+
+    // Returns the specified set of IP addresses as a string.
+    private String ip(Set<IpAddress> ipAddresses) {
+        Iterator<IpAddress> it = ipAddresses.iterator();
+        return it.hasNext() ? it.next().toString() : "unknown";
+    }
+
+    // Encodes the specified host location into a JSON object.
+    private ObjectNode location(ObjectMapper mapper, HostLocation location) {
+        return mapper.createObjectNode()
+                .put("device", location.deviceId().toString())
+                .put("port", location.port().toLong());
+    }
+
+    // Encodes the specified list of labels a JSON array.
+    private ArrayNode labels(ObjectMapper mapper, String... labels) {
+        ArrayNode json = mapper.createArrayNode();
+        for (String label : labels) {
+            json.add(label);
+        }
+        return json;
     }
 
     // Produces JSON structure from annotations.
@@ -225,28 +252,290 @@
     }
 
     // Produces a log message event bound to the client.
-    private String message(String severity, String message) {
-        return envelope("message",
+    private ObjectNode message(String severity, long id, String message) {
+        return envelope("message", id,
                         mapper.createObjectNode()
                                 .put("severity", severity)
                                 .put("message", message));
     }
 
     // Puts the payload into an envelope and returns it.
-    private String envelope(String type, ObjectNode payload) {
+    private ObjectNode envelope(String type, long sid, ObjectNode payload) {
         ObjectNode event = mapper.createObjectNode();
         event.put("event", type);
+        if (sid > 0) {
+            event.put("sid", sid);
+        }
         event.set("payload", payload);
-        return event.toString();
+        return event;
     }
 
-    @Override
-    public void event(TopologyEvent event) {
-        for (Event reason : event.reasons()) {
-            if (reason instanceof DeviceEvent) {
-                sendMessage(message((DeviceEvent) reason));
-            } else if (reason instanceof LinkEvent) {
-                sendMessage(message((LinkEvent) reason));
+    // Sends back device or host details.
+    private void showDetails(ObjectNode event) {
+        ObjectNode payload = payload(event);
+        String type = string(payload, "type", "unknown");
+        if (type.equals("device")) {
+            sendMessage(deviceDetails(deviceId(string(payload, "id")),
+                                      number(event, "sid")));
+        } else if (type.equals("host")) {
+            sendMessage(hostDetails(hostId(string(payload, "id")),
+                                    number(event, "sid")));
+        }
+    }
+
+    // Updates device/host meta information.
+    private void updateMetaInformation(ObjectNode event) {
+        ObjectNode payload = payload(event);
+        metaUi.put(string(payload, "id"), payload);
+    }
+
+    // Creates host-to-host intent.
+    private void createHostIntent(ObjectNode event) {
+        ObjectNode payload = payload(event);
+        long id = number(event, "sid");
+        // TODO: add protection against device ids and non-existent hosts.
+        HostId one = hostId(string(payload, "one"));
+        HostId two = hostId(string(payload, "two"));
+
+        HostToHostIntent hostIntent = new HostToHostIntent(appId, one, two,
+                                                           DefaultTrafficSelector.builder().build(),
+                                                           DefaultTrafficTreatment.builder().build());
+        intentsToMonitor.put(hostIntent.id(), number(event, "sid"));
+        intentService.submit(hostIntent);
+    }
+
+    // Sends traffic message.
+    private void sendTraffic(ObjectNode event) {
+        ObjectNode payload = payload(event);
+        long id = number(event, "sid");
+        IntentId intentId = IntentId.valueOf(payload.path("intentId").asLong());
+
+        if (payload != null) {
+            payload.put("traffic", true);
+            sendMessage(envelope("showPath", id, payload));
+        } else {
+            sendMessage(message("warn", id, "No path found"));
+        }
+    }
+
+    // Cancels sending traffic messages.
+    private void cancelTraffic(ObjectNode event) {
+        // TODO: implement this
+    }
+
+    // Finds the path between the specified devices.
+    private ObjectNode findPath(DeviceId one, DeviceId two) {
+        PathService pathService = directory.get(PathService.class);
+        Set<Path> paths = pathService.getPaths(one, two);
+        if (paths.isEmpty()) {
+            return null;
+        } else {
+            return pathMessage(paths.iterator().next());
+        }
+    }
+
+    // Produces a path message to the client.
+    private ObjectNode pathMessage(Path path) {
+        ObjectNode payload = mapper.createObjectNode();
+        ArrayNode links = mapper.createArrayNode();
+        for (Link link : path.links()) {
+            links.add(compactLinkString(link));
+        }
+
+        payload.set("links", links);
+        return payload;
+    }
+
+    /**
+     * Returns a compact string representing the given link.
+     *
+     * @param link infrastructure link
+     * @return formatted link string
+     */
+    public static String compactLinkString(Link link) {
+        return String.format(COMPACT, link.src().elementId(), link.src().port(),
+                             link.dst().elementId(), link.dst().port());
+    }
+
+
+    // Produces a link event message to the client.
+    private ObjectNode deviceMessage(DeviceEvent event) {
+        Device device = event.subject();
+        ObjectNode payload = mapper.createObjectNode()
+                .put("id", device.id().toString())
+                .put("type", device.type().toString().toLowerCase())
+                .put("online", deviceService.isAvailable(device.id()));
+
+        // Generate labels: id, chassis id, no-label, optional-name
+        ArrayNode labels = mapper.createArrayNode();
+        labels.add(device.id().toString());
+        labels.add(device.chassisId().toString());
+        labels.add(""); // compact no-label view
+        labels.add(device.annotations().value("name"));
+
+        // Add labels, props and stuff the payload into envelope.
+        payload.set("labels", labels);
+        payload.set("props", props(device.annotations()));
+        addMetaUi(device.id(), payload);
+
+        String type = (event.type() == DEVICE_ADDED) ? "addDevice" :
+                ((event.type() == DEVICE_REMOVED) ? "removeDevice" : "updateDevice");
+        return envelope(type, 0, payload);
+    }
+
+    // Produces a link event message to the client.
+    private ObjectNode linkMessage(LinkEvent event) {
+        Link link = event.subject();
+        ObjectNode payload = mapper.createObjectNode()
+                .put("id", compactLinkString(link))
+                .put("type", link.type().toString().toLowerCase())
+                .put("linkWidth", 2)
+                .put("src", link.src().deviceId().toString())
+                .put("srcPort", link.src().port().toString())
+                .put("dst", link.dst().deviceId().toString())
+                .put("dstPort", link.dst().port().toString());
+        String type = (event.type() == LINK_ADDED) ? "addLink" :
+                ((event.type() == LINK_REMOVED) ? "removeLink" : "updateLink");
+        return envelope(type, 0, payload);
+    }
+
+    // Produces a host event message to the client.
+    private ObjectNode hostMessage(HostEvent event) {
+        Host host = event.subject();
+        ObjectNode payload = mapper.createObjectNode()
+                .put("id", host.id().toString())
+                .put("ingress", compactLinkString(edgeLink(host, true)))
+                .put("egress", compactLinkString(edgeLink(host, false)));
+        payload.set("cp", location(mapper, host.location()));
+        payload.set("labels", labels(mapper, ip(host.ipAddresses()),
+                                     host.mac().toString()));
+        payload.set("props", props(host.annotations()));
+        addMetaUi(host.id(), payload);
+
+        String type = (event.type() == HOST_ADDED) ? "addHost" :
+                ((event.type() == HOST_REMOVED) ? "removeHost" : "updateHost");
+        return envelope(type, 0, payload);
+    }
+
+    private DefaultEdgeLink edgeLink(Host host, boolean ingress) {
+        return new DefaultEdgeLink(PID, new ConnectPoint(host.id(), portNumber(0)),
+                                   host.location(), ingress);
+    }
+
+    private void addMetaUi(ElementId id, ObjectNode payload) {
+        ObjectNode meta = metaUi.get(id.toString());
+        if (meta != null) {
+            payload.set("metaUi", meta);
+        }
+    }
+
+
+    // Returns device details response.
+    private ObjectNode deviceDetails(DeviceId deviceId, long sid) {
+        Device device = deviceService.getDevice(deviceId);
+        Annotations annot = device.annotations();
+        int portCount = deviceService.getPorts(deviceId).size();
+        return envelope("showDetails", sid,
+                        json(deviceId.toString(),
+                             device.type().toString().toLowerCase(),
+                             new Prop("Name", annot.value("name")),
+                             new Prop("Vendor", device.manufacturer()),
+                             new Prop("H/W Version", device.hwVersion()),
+                             new Prop("S/W Version", device.swVersion()),
+                             new Prop("Serial Number", device.serialNumber()),
+                             new Separator(),
+                             new Prop("Latitude", annot.value("latitude")),
+                             new Prop("Longitude", annot.value("longitude")),
+                             new Prop("Ports", Integer.toString(portCount))));
+    }
+
+    // Returns host details response.
+    private ObjectNode hostDetails(HostId hostId, long sid) {
+        Host host = hostService.getHost(hostId);
+        Annotations annot = host.annotations();
+        return envelope("showDetails", sid,
+                        json(hostId.toString(), "host",
+                             new Prop("MAC", host.mac().toString()),
+                             new Prop("IP", host.ipAddresses().toString()),
+                             new Separator(),
+                             new Prop("Latitude", annot.value("latitude")),
+                             new Prop("Longitude", annot.value("longitude"))));
+    }
+
+    // Produces JSON property details.
+    private ObjectNode json(String id, String type, Prop... props) {
+        ObjectMapper mapper = new ObjectMapper();
+        ObjectNode result = mapper.createObjectNode()
+                .put("id", id).put("type", type);
+        ObjectNode pnode = mapper.createObjectNode();
+        ArrayNode porder = mapper.createArrayNode();
+        for (Prop p : props) {
+            porder.add(p.key);
+            pnode.put(p.key, p.value);
+        }
+        result.set("propOrder", porder);
+        result.set("props", pnode);
+        return result;
+    }
+
+    // Auxiliary key/value carrier.
+    private class Prop {
+        private final String key;
+        private final String value;
+
+        protected Prop(String key, String value) {
+            this.key = key;
+            this.value = value;
+        }
+    }
+
+    private class Separator extends Prop {
+        protected Separator() {
+            super("-", "");
+        }
+    }
+
+    private class InternalDeviceListener implements DeviceListener {
+        @Override
+        public void event(DeviceEvent event) {
+            sendMessage(deviceMessage(event));
+        }
+    }
+
+    private class InternalLinkListener implements LinkListener {
+        @Override
+        public void event(LinkEvent event) {
+            sendMessage(linkMessage(event));
+        }
+    }
+
+    private class InternalHostListener implements HostListener {
+        @Override
+        public void event(HostEvent event) {
+            sendMessage(hostMessage(event));
+        }
+    }
+
+    private class InternalMastershipListener implements MastershipListener {
+        @Override
+        public void event(MastershipEvent event) {
+
+        }
+    }
+
+    private class InternalIntentListener implements IntentListener {
+        @Override
+        public void event(IntentEvent event) {
+            Intent intent = event.subject();
+            Long sid = intentsToMonitor.get(intent.id());
+            if (sid != null) {
+                List<Intent> installable = intentService.getInstallableIntents(intent.id());
+                if (installable != null && !installable.isEmpty()) {
+                    PathIntent pathIntent = (PathIntent) installable.iterator().next();
+                    Path path = pathIntent.path();
+                    ObjectNode payload = pathMessage(path).put("intentId", intent.id().toString());
+                    sendMessage(envelope("showPath", sid, payload));
+                }
             }
         }
     }
diff --git a/web/gui/src/main/webapp/index2.html b/web/gui/src/main/webapp/index2.html
index 6168271..9688522 100644
--- a/web/gui/src/main/webapp/index2.html
+++ b/web/gui/src/main/webapp/index2.html
@@ -90,6 +90,7 @@
     <script src="sampleAlt2.js"></script>
     <script src="sampleRadio.js"></script>
     <script src="sampleKeys.js"></script>
+    <script src="sampleHash.js"></script>
 
     <!-- Contributed (application) views injected here -->
     <!-- TODO: replace with template marker and inject refs server-side -->
diff --git a/web/gui/src/main/webapp/json/intent/ev_1_ui.json b/web/gui/src/main/webapp/json/ev/intent/ev_1_ui.json
similarity index 100%
rename from web/gui/src/main/webapp/json/intent/ev_1_ui.json
rename to web/gui/src/main/webapp/json/ev/intent/ev_1_ui.json
diff --git a/web/gui/src/main/webapp/json/intent/ev_2_onos.json b/web/gui/src/main/webapp/json/ev/intent/ev_2_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/intent/ev_2_onos.json
rename to web/gui/src/main/webapp/json/ev/intent/ev_2_onos.json
diff --git a/web/gui/src/main/webapp/json/intent/ev_3_ui.json b/web/gui/src/main/webapp/json/ev/intent/ev_3_ui.json
similarity index 100%
rename from web/gui/src/main/webapp/json/intent/ev_3_ui.json
rename to web/gui/src/main/webapp/json/ev/intent/ev_3_ui.json
diff --git a/web/gui/src/main/webapp/json/intent/ev_4_onos.json b/web/gui/src/main/webapp/json/ev/intent/ev_4_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/intent/ev_4_onos.json
rename to web/gui/src/main/webapp/json/ev/intent/ev_4_onos.json
diff --git a/web/gui/src/main/webapp/json/intent/ev_5_onos.json b/web/gui/src/main/webapp/json/ev/intent/ev_5_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/intent/ev_5_onos.json
rename to web/gui/src/main/webapp/json/ev/intent/ev_5_onos.json
diff --git a/web/gui/src/main/webapp/json/intent/ev_6_onos.json b/web/gui/src/main/webapp/json/ev/intent/ev_6_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/intent/ev_6_onos.json
rename to web/gui/src/main/webapp/json/ev/intent/ev_6_onos.json
diff --git a/web/gui/src/main/webapp/json/intent/ev_7_ui.json b/web/gui/src/main/webapp/json/ev/intent/ev_7_ui.json
similarity index 100%
rename from web/gui/src/main/webapp/json/intent/ev_7_ui.json
rename to web/gui/src/main/webapp/json/ev/intent/ev_7_ui.json
diff --git a/web/gui/src/main/webapp/json/ev/intent/scenario.json b/web/gui/src/main/webapp/json/ev/intent/scenario.json
new file mode 100644
index 0000000..136d027
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/intent/scenario.json
@@ -0,0 +1,9 @@
+{
+  "comments": [
+    "This scenario steps through adding a host intent."
+  ],
+  "title": "Host Intent Scenario",
+  "params": {
+    "lastAuto": 0
+  }
+}
\ No newline at end of file
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_1_onos.json b/web/gui/src/main/webapp/json/ev/simple/ev_1_onos.json
new file mode 100644
index 0000000..1776f94
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_1_onos.json
@@ -0,0 +1,17 @@
+{
+  "event": "addDevice",
+  "payload": {
+    "id": "of:0000ffffffff0008",
+    "type": "switch",
+    "online": false,
+    "labels": [
+      "0000ffffffff0008",
+      "FF:FF:FF:FF:00:08",
+      "sw-8"
+    ],
+    "metaUi": {
+      "x": 400,
+      "y": 280
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_2_onos.json b/web/gui/src/main/webapp/json/ev/simple/ev_2_onos.json
new file mode 100644
index 0000000..4f9b32a
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_2_onos.json
@@ -0,0 +1,17 @@
+{
+  "event": "addDevice",
+  "payload": {
+    "id": "of:0000ffffffff0003",
+    "type": "switch",
+    "online": false,
+    "labels": [
+      "0000ffffffff0003",
+      "FF:FF:FF:FF:00:03",
+      "sw-3"
+    ],
+    "metaUi": {
+      "x": 800,
+      "y": 280
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_3_onos.json b/web/gui/src/main/webapp/json/ev/simple/ev_3_onos.json
new file mode 100644
index 0000000..3312682
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_3_onos.json
@@ -0,0 +1,14 @@
+{
+  "event": "addLink",
+  "payload": {
+    "src": "of:0000ffffffff0003",
+    "srcPort": "21",
+    "dst": "of:0000ffffffff0008",
+    "dstPort": "20",
+    "type": "infra",
+    "linkWidth": 2,
+    "props" : {
+      "BW": "70 G"
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_4_onos.json b/web/gui/src/main/webapp/json/ev/simple/ev_4_onos.json
new file mode 100644
index 0000000..51fdb8c
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_4_onos.json
@@ -0,0 +1,16 @@
+{
+  "event": "addHost",
+  "payload": {
+    "id": "00:00:00:00:00:03/-1",
+    "cp": {
+      "device": "of:0000ffffffff0003",
+      "port": 1
+    },
+    "labels": [
+      "10.0.0.3",
+      "00:00:00:00:00:03"
+    ],
+    "metaUi": {
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_5_onos.json b/web/gui/src/main/webapp/json/ev/simple/ev_5_onos.json
new file mode 100644
index 0000000..a6489b2
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_5_onos.json
@@ -0,0 +1,16 @@
+{
+  "event": "addHost",
+  "payload": {
+    "id": "00:00:00:00:00:08/-1",
+    "cp": {
+      "device": "of:0000ffffffff0008",
+      "port": 1
+    },
+    "labels": [
+      "10.0.0.8",
+      "00:00:00:00:00:08"
+    ],
+    "metaUi": {
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/simple/scenario.json b/web/gui/src/main/webapp/json/ev/simple/scenario.json
new file mode 100644
index 0000000..d24626f
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/simple/scenario.json
@@ -0,0 +1,9 @@
+{
+  "comments": [
+    "Add two devices and one link (auto), and two hosts."
+  ],
+  "title": "Simple Startup Scenario",
+  "params": {
+    "lastAuto": 0
+  }
+}
\ No newline at end of file
diff --git a/web/gui/src/main/webapp/json/eventTest_10.json b/web/gui/src/main/webapp/json/ev/startup/ev_10_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_10.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_10_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_11.json b/web/gui/src/main/webapp/json/ev/startup/ev_11_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_11.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_11_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_12.json b/web/gui/src/main/webapp/json/ev/startup/ev_12_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_12.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_12_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_13.json b/web/gui/src/main/webapp/json/ev/startup/ev_13_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_13.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_13_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_14.json b/web/gui/src/main/webapp/json/ev/startup/ev_14_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_14.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_14_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_15.json b/web/gui/src/main/webapp/json/ev/startup/ev_15_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_15.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_15_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_16.json b/web/gui/src/main/webapp/json/ev/startup/ev_16_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_16.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_16_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_17.json b/web/gui/src/main/webapp/json/ev/startup/ev_17_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_17.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_17_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_18.json b/web/gui/src/main/webapp/json/ev/startup/ev_18_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_18.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_18_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_19.json b/web/gui/src/main/webapp/json/ev/startup/ev_19_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_19.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_19_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_1.json b/web/gui/src/main/webapp/json/ev/startup/ev_1_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_1.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_1_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_20.json b/web/gui/src/main/webapp/json/ev/startup/ev_20_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_20.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_20_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_21.json b/web/gui/src/main/webapp/json/ev/startup/ev_21_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_21.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_21_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_22.json b/web/gui/src/main/webapp/json/ev/startup/ev_22_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_22.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_22_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_23.json b/web/gui/src/main/webapp/json/ev/startup/ev_23_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_23.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_23_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_24.json b/web/gui/src/main/webapp/json/ev/startup/ev_24_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_24.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_24_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_25.json b/web/gui/src/main/webapp/json/ev/startup/ev_25_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_25.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_25_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_26.json b/web/gui/src/main/webapp/json/ev/startup/ev_26_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_26.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_26_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_27.json b/web/gui/src/main/webapp/json/ev/startup/ev_27_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_27.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_27_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_28.json b/web/gui/src/main/webapp/json/ev/startup/ev_28_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_28.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_28_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_29.json b/web/gui/src/main/webapp/json/ev/startup/ev_29_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_29.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_29_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_2.json b/web/gui/src/main/webapp/json/ev/startup/ev_2_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_2.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_2_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_30.json b/web/gui/src/main/webapp/json/ev/startup/ev_30_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_30.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_30_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_31.json b/web/gui/src/main/webapp/json/ev/startup/ev_31_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_31.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_31_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_32.json b/web/gui/src/main/webapp/json/ev/startup/ev_32_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_32.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_32_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_33.json b/web/gui/src/main/webapp/json/ev/startup/ev_33_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_33.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_33_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_34.json b/web/gui/src/main/webapp/json/ev/startup/ev_34_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_34.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_34_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_35.json b/web/gui/src/main/webapp/json/ev/startup/ev_35_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_35.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_35_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_3.json b/web/gui/src/main/webapp/json/ev/startup/ev_3_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_3.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_3_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_4.json b/web/gui/src/main/webapp/json/ev/startup/ev_4_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_4.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_4_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_5.json b/web/gui/src/main/webapp/json/ev/startup/ev_5_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_5.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_5_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_6.json b/web/gui/src/main/webapp/json/ev/startup/ev_6_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_6.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_6_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_7.json b/web/gui/src/main/webapp/json/ev/startup/ev_7_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_7.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_7_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_8.json b/web/gui/src/main/webapp/json/ev/startup/ev_8_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_8.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_8_onos.json
diff --git a/web/gui/src/main/webapp/json/eventTest_9.json b/web/gui/src/main/webapp/json/ev/startup/ev_9_onos.json
similarity index 100%
rename from web/gui/src/main/webapp/json/eventTest_9.json
rename to web/gui/src/main/webapp/json/ev/startup/ev_9_onos.json
diff --git a/web/gui/src/main/webapp/json/ev/startup/scenario.json b/web/gui/src/main/webapp/json/ev/startup/scenario.json
new file mode 100644
index 0000000..37939ca0
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/startup/scenario.json
@@ -0,0 +1,10 @@
+{
+  "comments": [
+    "This scenario steps through adding devices and links.",
+    "(Typical 'start-ip' of the view.)"
+  ],
+  "title": "Startup Scenario",
+  "params": {
+    "lastAuto": 32
+  }
+}
\ No newline at end of file
diff --git a/web/gui/src/main/webapp/onos2.js b/web/gui/src/main/webapp/onos2.js
index 4aeb23e..a0276df 100644
--- a/web/gui/src/main/webapp/onos2.js
+++ b/web/gui/src/main/webapp/onos2.js
@@ -52,6 +52,7 @@
             current = {
                 view: null,
                 ctx: '',
+                flags: {},
                 theme: settings.theme
             },
             built = false,
@@ -110,6 +111,7 @@
         function doError(msg) {
             errorCount++;
             console.error(msg);
+            doAlert(msg);
         }
 
         function trace(msg) {
@@ -140,7 +142,7 @@
 
             t = parseHash(hash);
             if (!t || !t.vid) {
-                doError('Unable to parse target hash: ' + hash);
+                doError('Unable to parse target hash: "' + hash + '"');
             }
 
             view = views[t.vid];
@@ -160,33 +162,72 @@
 
         function parseHash(s) {
             // extract navigation coordinates from the supplied string
-            // "vid,ctx" --> { vid:vid, ctx:ctx }
+            // "vid,ctx?flag1,flag2" --> { vid:vid, ctx:ctx, flags:{...} }
             traceFn('parseHash', s);
 
-            var m = /^[#]{0,1}(\S+),(\S*)$/.exec(s);
+            // look for use of flags, first
+            var vidctx,
+                vid,
+                ctx,
+                flags,
+                flagMap,
+                m;
+
+            // RE that includes flags ('?flag1,flag2')
+            m = /^[#]{0,1}(.+)\?(.+)$/.exec(s);
             if (m) {
-                return { vid: m[1], ctx: m[2] };
+                vidctx = m[1];
+                flags = m[2];
+                flagMap = {};
+            } else {
+                // no flags
+                m = /^[#]{0,1}((.+)(,.+)*)$/.exec(s);
+                if (m) {
+                    vidctx = m[1];
+                } else {
+                    // bad hash
+                    return null;
+                }
             }
 
-            m = /^[#]{0,1}(\S+)$/.exec(s);
-            return m ? { vid: m[1] } : null;
+            vidctx = vidctx.split(',');
+            vid = vidctx[0];
+            ctx = vidctx[1];
+            if (flags) {
+                flags.split(',').forEach(function (f) {
+                    flagMap[f.trim()] = true;
+                });
+            }
+
+            return {
+                vid: vid.trim(),
+                ctx: ctx ? ctx.trim() : '',
+                flags: flagMap
+            };
+
         }
 
-        function makeHash(t, ctx) {
+        function makeHash(t, ctx, flags) {
             traceFn('makeHash');
-            // make a hash string from the given navigation coordinates.
+            // make a hash string from the given navigation coordinates,
+            // and optional flags map.
             // if t is not an object, then it is a vid
             var h = t,
-                c = ctx || '';
+                c = ctx || '',
+                f = $.isPlainObject(flags) ? flags : null;
 
             if ($.isPlainObject(t)) {
                 h = t.vid;
                 c = t.ctx || '';
+                f = t.flags || null;
             }
 
             if (c) {
                 h += ',' + c;
             }
+            if (f) {
+                h += '?' + d3.map(f).keys().join(',');
+            }
             trace('hash = "' + h + '"');
             return h;
         }
@@ -244,6 +285,9 @@
             // set the specified view as current, while invoking the
             // appropriate life-cycle callbacks
 
+            // first, we'll start by closing the alerts pane, if open
+            closeAlerts();
+
             // if there is a current view, and it is not the same as
             // the incoming view, then unload it...
             if (current.view && (current.view.vid !== view.vid)) {
@@ -258,10 +302,11 @@
             // cache new view and context
             current.view = view;
             current.ctx = t.ctx || '';
+            current.flags = t.flags || {};
 
             // preload is called only once, after the view is in the DOM
             if (!view.preloaded) {
-                view.preload(current.ctx);
+                view.preload(current.ctx, current.flags);
                 view.preloaded = true;
             }
 
@@ -269,7 +314,7 @@
             view.reset();
 
             // load the view
-            view.load(current.ctx);
+            view.load(current.ctx, current.flags);
         }
 
         // generate 'unique' id by prefixing view id
@@ -454,7 +499,7 @@
             d3.selectAll('.onosView').call(setViewDimensions);
             // allow current view to react to resize event...
             if (current.view) {
-                current.view.resize(current.ctx);
+                current.view.resize(current.ctx, current.flags);
             }
         }
 
@@ -521,13 +566,13 @@
                 }
             },
 
-            preload: function (ctx) {
+            preload: function (ctx, flags) {
                 var c = ctx || '',
                     fn = isF(this.cb.preload);
                 traceFn('View.preload', this.vid + ', ' + c);
                 if (fn) {
                     trace('PRELOAD cb for ' + this.vid);
-                    fn(this.token(), c);
+                    fn(this.token(), c, flags);
                 }
             },
 
@@ -544,15 +589,14 @@
                 }
             },
 
-            load: function (ctx) {
+            load: function (ctx, flags) {
                 var c = ctx || '',
                     fn = isF(this.cb.load);
                 traceFn('View.load', this.vid + ', ' + c);
                 this.$div.classed('currentView', true);
-                // TODO: add radio button set, if needed
                 if (fn) {
                     trace('LOAD cb for ' + this.vid);
-                    fn(this.token(), c);
+                    fn(this.token(), c, flags);
                 }
             },
 
@@ -560,14 +604,13 @@
                 var fn = isF(this.cb.unload);
                 traceFn('View.unload', this.vid);
                 this.$div.classed('currentView', false);
-                // TODO: remove radio button set, if needed
                 if (fn) {
                     trace('UNLOAD cb for ' + this.vid);
                     fn(this.token());
                 }
             },
 
-            resize: function (ctx) {
+            resize: function (ctx, flags) {
                 var c = ctx || '',
                     fn = isF(this.cb.resize),
                     w = this.width(),
@@ -576,7 +619,7 @@
                         ' [' + w + 'x' + h + ']');
                 if (fn) {
                     trace('RESIZE cb for ' + this.vid);
-                    fn(this.token(), c);
+                    fn(this.token(), c, flags);
                 }
             },
 
diff --git a/web/gui/src/main/webapp/preamble.js b/web/gui/src/main/webapp/preamble.js
index 8ee8e45..b6a8c02 100644
--- a/web/gui/src/main/webapp/preamble.js
+++ b/web/gui/src/main/webapp/preamble.js
@@ -21,12 +21,17 @@
  */
 
 (function () {
+
+    // NOTE: DON'T Want to do this.. we want to be able to
+    //  use the parameter section, for example:
+    //   #viewId,context?flag1,flag2,flag3
+
     // Check if the URL in the address bar contains a parameter section
     // (delineated by '?'). If this is the case, rewrite using '#' instead.
 
-    var m = /([^?]*)\?(.*)/.exec(window.location.href);
-    if (m) {
-        window.location.href = m[1] + '#' + m[2];
-    }
+    //var m = /([^?]*)\?(.*)/.exec(window.location.href);
+    //if (m) {
+    //    window.location.href = m[1] + '#' + m[2];
+    //}
 
 }());
diff --git a/web/gui/src/main/webapp/sampleHash.js b/web/gui/src/main/webapp/sampleHash.js
new file mode 100644
index 0000000..c94ebb8
--- /dev/null
+++ b/web/gui/src/main/webapp/sampleHash.js
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ Sample view to illustrate hash formats.
+
+ @author Simon Hunt
+ */
+
+(function (onos) {
+    'use strict';
+
+    var intro = "Try using the following hashes in the address bar:",
+        hashPrefix = '#sampleHash',
+        suffixes = [
+            '',
+            ',one',
+            ',two',
+            ',context,ignored',
+            ',context,ignored?a,b,c',
+            ',two?foo',
+            ',three?foo,bar'
+        ],
+        $d;
+
+    function note(txt) {
+        $d.append('p')
+            .text(txt)
+            .style({
+                'font-size': '10pt',
+                color: 'darkorange',
+                padding: '0 20px',
+                margin: 0
+            });
+    }
+
+    function para(txt, color) {
+        var c = color || 'black';
+        $d.append('p')
+            .text(txt)
+            .style({
+                padding: '2px 8px',
+                color: c
+            });
+    }
+
+    function load(view, ctx, flags) {
+        var c = ctx || '(undefined)',
+            f = flags ? d3.map(flags).keys() : [];
+
+        $d = view.$div;
+
+        para(intro);
+
+        suffixes.forEach(function (s) {
+            note(hashPrefix + s);
+        });
+
+        para('View ID: ' + view.vid, 'blue');
+        para('Context: ' + c, 'blue');
+        para('Flags: { ' + f.join(', ') + ' }', 'magenta');
+    }
+
+    // == register the view here, with links to lifecycle callbacks
+
+    onos.ui.addView('sampleHash', {
+        reset: true,    // empty the div on reset
+        load: load
+    });
+
+}(ONOS));
diff --git a/web/gui/src/main/webapp/topo2.css b/web/gui/src/main/webapp/topo2.css
index c4d9a2d..a5bf661 100644
--- a/web/gui/src/main/webapp/topo2.css
+++ b/web/gui/src/main/webapp/topo2.css
@@ -20,55 +20,64 @@
  @author Simon Hunt
  */
 
-svg #topo-bg {
+#topo svg #topo-bg {
     opacity: 0.5;
 }
 
 /* NODES */
 
-svg .node.device {
+#topo svg .node.device {
     stroke: none;
     stroke-width: 1.5px;
     cursor: pointer;
 }
 
-svg .node.device rect {
+#topo svg .node.device rect {
     stroke-width: 1.5px;
 }
 
-svg .node.device.fixed rect {
+#topo svg .node.device.fixed rect {
     stroke-width: 1.5;
     stroke: #ccc;
 }
 
-svg .node.device.switch {
+#topo svg .node.device.switch {
     fill: #17f;
 }
 
-svg .node.device.roadm {
+#topo svg .node.device.roadm {
     fill: #03c;
 }
 
-svg .node text {
+#topo svg .node.host {
+    fill: #846;
+}
+
+#topo svg .node text {
     stroke: none;
     fill: white;
     font: 10pt sans-serif;
     pointer-events: none;
 }
 
-svg .node.selected rect,
-svg .node.selected circle {
+#topo svg .node.selected rect,
+#topo svg .node.selected circle {
     filter: url(#blue-glow);
 }
 
 /* LINKS */
 
-svg .link {
+#topo svg .link {
     opacity: .7;
 }
 
+#topo svg .link.showPath {
+    stroke: #f00;
+    stroke-width: 4px;
+}
+
 /* for debugging */
-svg .node circle.debug {
+#topo svg .node circle.debug {
     fill: white;
     stroke: red;
 }
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index acd2464..18aa917 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -132,8 +132,21 @@
             links: [],
             lookup: {}
         },
+        scenario = {
+            evDir: 'json/ev/',
+            evScenario: '/scenario.json',
+            evPrefix: '/ev_',
+            evOnos: '_onos.json',
+            evUi: '_ui.json',
+            ctx: null,
+            params: {},
+            evNumber: 0,
+            view: null,
+            debug: false
+        },
         webSock,
-        labelIdx = 0,
+        deviceLabelIndex = 0,
+        hostLabelIndex = 0,
 
         selectOrder = [],
         selections = {},
@@ -155,10 +168,6 @@
     // ==============================
     // For Debugging / Development
 
-    var eventPrefix = 'json/eventTest_',
-        eventNumber = 0,
-        alertNumber = 0;
-
     function note(label, msg) {
         console.log('NOTE: ' + label + ': ' + msg);
     }
@@ -175,32 +184,71 @@
         view.alert('test');
     }
 
-    function injectTestEvent(view) {
+    function abortIfLive() {
         if (config.useLiveData) {
-            view.alert("Sorry, currently using live data..");
-            return;
+            scenario.view.alert("Sorry, currently using live data..");
+            return true;
         }
+        return false;
+    }
 
-        eventNumber++;
-        var eventUrl = eventPrefix + eventNumber + '.json';
+    function testDebug(msg) {
+        if (scenario.debug) {
+            scenario.view.alert(msg);
+        }
+    }
 
-        d3.json(eventUrl, function(err, data) {
+    function injectTestEvent(view) {
+        if (abortIfLive()) { return; }
+        var sc = scenario,
+            evn = ++sc.evNumber,
+            pfx = sc.evDir + sc.ctx + sc.evPrefix + evn,
+            onosUrl = pfx + sc.evOnos,
+            uiUrl = pfx + sc.evUi;
+
+        tryOnosEvent(onosUrl, uiUrl);
+    }
+
+    // TODO: tryOnosEvent/tryUiEvent folded into recursive function.
+    function tryOnosEvent(onosUrl, uiUrl) {
+        var v = scenario.view;
+        d3.json(onosUrl, function(err, data) {
             if (err) {
-                view.dataLoadError(err, eventUrl);
+                if (err.status === 404) {
+                    tryUiEvent(uiUrl);
+                } else {
+                    v.alert('non-404 error:\n\n' + onosUrl + '\n\n' + err);
+                }
             } else {
+                testDebug('loaded: ' + onosUrl);
                 handleServerEvent(data);
             }
         });
     }
 
-    function injectStartupEvents(view) {
-        if (config.useLiveData) {
-            view.alert("Sorry, currently using live data..");
-            return;
-        }
+    function tryUiEvent(uiUrl) {
+        var v = scenario.view;
+        d3.json(uiUrl, function(err, data) {
+            if (err) {
+                v.alert('Error:\n\n' + uiUrl + '\n\n' +
+                        err.status + ': ' + err.statusText);
+            } else {
+                testDebug('loaded: ' + uiUrl);
+                handleUiEvent(data);
+            }
+        });
+    }
 
-        var lastStartupEvent = 32;
-        while (eventNumber < lastStartupEvent) {
+    function handleUiEvent(data) {
+        testDebug('handleUiEvent(): ' + data.event);
+        // TODO:
+    }
+
+    function injectStartupEvents(view) {
+        var last = scenario.params.lastAuto || 0;
+        if (abortIfLive()) { return; }
+
+        while (scenario.evNumber < last) {
             injectTestEvent(view);
         }
     }
@@ -211,14 +259,16 @@
     }
 
     function cycleLabels() {
-        labelIdx = (labelIdx === network.deviceLabelCount - 1) ? 0 : labelIdx + 1;
+        deviceLabelIndex = (deviceLabelIndex === network.deviceLabelCount - 1) ? 0 : deviceLabelIndex + 1;
 
         function niceLabel(label) {
             return (label && label.trim()) ? label : '.';
         }
 
         network.nodes.forEach(function (d) {
-            var idx = (labelIdx < d.labels.length) ? labelIdx : 0,
+            if (d.class !== 'device') { return; }
+
+            var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0,
                 node = d3.select('#' + safeId(d.id)),
                 box;
 
@@ -303,9 +353,14 @@
 
     var eventDispatch = {
         addDevice: addDevice,
-        updateDevice: updateDevice,
-        removeDevice: removeDevice,
+        updateDevice: stillToImplement,
+        removeDevice: stillToImplement,
         addLink: addLink,
+        updateLink: stillToImplement,
+        removeLink: stillToImplement,
+        addHost: addHost,
+        updateHost: stillToImplement,
+        removeHost: stillToImplement,
         showPath: showPath
     };
 
@@ -320,18 +375,6 @@
         network.force.start();
     }
 
-    function updateDevice(data) {
-        var device = data.payload;
-        note('updateDevice', device.id);
-
-    }
-
-    function removeDevice(data) {
-        var device = data.payload;
-        note('removeDevice', device.id);
-
-    }
-
     function addLink(data) {
         var link = data.payload,
             lnk = createLink(link);
@@ -340,16 +383,57 @@
             note('addLink', lnk.id);
 
             network.links.push(lnk);
+            network.lookup[lnk.id] = lnk;
             updateLinks();
             network.force.start();
         }
     }
 
-    function showPath(data) {
-        network.view.alert(data.event + "\n" + data.payload.links.length);
+    function addHost(data) {
+        var host = data.payload,
+            node = createHostNode(host),
+            lnk;
+
+        note('addHost', node.id);
+        network.nodes.push(node);
+        network.lookup[host.id] = node;
+        updateNodes();
+
+        lnk = createHostLink(host);
+        if (lnk) {
+            network.links.push(lnk);
+            network.lookup[host.ingress] = lnk;
+            network.lookup[host.egress] = lnk;
+            updateLinks();
+        }
+        network.force.start();
     }
 
-    // ....
+    function showPath(data) {
+        var links = data.payload.links,
+            s = [ data.event + "\n" + links.length ];
+        links.forEach(function (d, i) {
+            s.push(d);
+        });
+        network.view.alert(s.join('\n'));
+
+        links.forEach(function (d, i) {
+            var link = network.lookup[d];
+            if (link) {
+                d3.select('#' + link.svgId).classed('showPath', true);
+            }
+        });
+
+        // TODO: add selection-highlite lines to links
+    }
+
+    // ...............................
+
+    function stillToImplement(data) {
+        var p = data.payload;
+        note(data.event, p.id);
+        //network.view.alert('Not yet implemented: "' + data.event + '"');
+    }
 
     function unknownEvent(data) {
         network.view.alert('Unknown event type: "' + data.event + '"');
@@ -367,10 +451,42 @@
         return 'translate(' + x + ',' + y + ')';
     }
 
+    function createHostLink(host) {
+        var src = host.id,
+            dst = host.cp.device,
+            id = host.id,
+            srcNode = network.lookup[src],
+            dstNode = network.lookup[dst],
+            lnk;
+
+        if (!dstNode) {
+            // TODO: send warning message back to server on websocket
+            network.view.alert('switch not on map for link\n\n' +
+            'src = ' + src + '\ndst = ' + dst);
+            return null;
+        }
+
+        lnk = {
+            svgId: safeId(src) + '-' + safeId(dst),
+            id: id,
+            source: srcNode,
+            target: dstNode,
+            class: 'link',
+            svgClass: 'link hostLink',
+            x1: srcNode.x,
+            y1: srcNode.y,
+            x2: dstNode.x,
+            y2: dstNode.y,
+            width: 1
+        };
+        return lnk;
+    }
+
     function createLink(link) {
         var type = link.type,
             src = link.src,
             dst = link.dst,
+            id = link.id,
             w = link.linkWidth,
             srcNode = network.lookup[src],
             dstNode = network.lookup[dst],
@@ -384,7 +500,8 @@
         }
 
         lnk = {
-                id: safeId(src) + '~' + safeId(dst),
+                svgId: safeId(src) + '-' + safeId(dst),
+                id: id,
                 source: srcNode,
                 target: dstNode,
                 class: 'link',
@@ -415,7 +532,7 @@
         var entering = link.enter()
             .append('line')
             .attr({
-                id: function (d) { return d.id; },
+                id: function (d) { return d.svgId; },
                 class: function (d) { return d.svgClass; },
                 x1: function (d) { return d.x1; },
                 y1: function (d) { return d.y1; },
@@ -433,6 +550,19 @@
         // augment links
         // TODO: add src/dst port labels etc.
 
+
+        // operate on both existing and new links, if necessary
+        //link .foo() .bar() ...
+
+        // operate on exiting links:
+        // TODO: figure out how to remove the node 'g' AND its children
+        link.exit()
+            .transition()
+            .duration(750)
+            .attr({
+                opacity: 0
+            })
+            .remove();
     }
 
     function createDeviceNode(device) {
@@ -451,6 +581,22 @@
         return node;
     }
 
+    function createHostNode(host) {
+        // start with the object as is
+        var node = host;
+
+        // Augment as needed...
+        node.class = 'host';
+        node.svgClass = 'node host';
+        // TODO: consider placing near its switch, if [x,y] not defined
+        positionNode(node);
+
+        // cache label array length
+        network.hostLabelCount = host.labels.length;
+
+        return node;
+    }
+
     function positionNode(node) {
         var meta = node.metaUi,
             x = 0,
@@ -525,7 +671,7 @@
         entering.filter('.device').each(function (d) {
             var node = d3.select(this),
                 icon = iconUrl(d),
-                idx = (labelIdx < d.labels.length) ? labelIdx : 0,
+                idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0,
                 box;
 
             node.append('rect')
@@ -568,6 +714,32 @@
             }
         });
 
+        // augment host nodes...
+        entering.filter('.host').each(function (d) {
+            var node = d3.select(this),
+                idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0,
+                box;
+
+            node.append('circle')
+                .attr('r', 8);     // TODO: define host circle radius
+
+            // TODO: are we attaching labels to hosts?
+            node.append('text')
+                .text(d.labels[idx])
+                .attr('dy', '1.1em');
+
+            // debug function to show the modelled x,y coordinates of nodes...
+            if (debug('showNodeXY')) {
+                node.select('circle').attr('fill-opacity', 0.5);
+                node.append('circle')
+                    .attr({
+                        class: 'debug',
+                        cx: 0,
+                        cy: 0,
+                        r: '3px'
+                    });
+            }
+        });
 
         // operate on both existing and new nodes, if necessary
         //node .foo() .bar() ...
@@ -618,7 +790,6 @@
             webSock.ws = new WebSocket(webSockUrl());
 
             webSock.ws.onopen = function() {
-                webSock._send("Hi there!");
             };
 
             webSock.ws.onmessage = function(m) {
@@ -643,7 +814,7 @@
             if (webSock.ws) {
                 webSock.ws.send(message);
             } else {
-                network.view.alert('no web socket open');
+                network.view.alert('no web socket open\n\n' + message);
             }
         }
 
@@ -714,7 +885,7 @@
         //flyinPane(null);
     }
 
-
+    // TODO: this click handler does not get unloaded when the view does
     $('#view').on('click', function(e) {
         if (!$(e.target).closest('.node').length) {
             if (!e.metaKey) {
@@ -723,6 +894,33 @@
         }
     });
 
+
+    function prepareScenario(view, ctx, dbg) {
+        var sc = scenario,
+            urlSc = sc.evDir + ctx + sc.evScenario;
+
+        if (!ctx) {
+            view.alert("No scenario specified (null ctx)");
+            return;
+        }
+
+        sc.view = view;
+        sc.ctx = ctx;
+        sc.debug = dbg;
+        sc.evNumber = 0;
+
+        d3.json(urlSc, function(err, data) {
+            var p = data && data.params || {};
+            if (err) {
+                view.alert('No scenario found:\n\n' + urlSc + '\n\n' + err);
+            } else {
+                sc.params = p;
+                view.alert("Scenario loaded: " + ctx + '\n\n' + data.title);
+            }
+        });
+
+    }
+
     // ==============================
     // View life-cycle callbacks
 
@@ -767,10 +965,12 @@
         node = nodeG.selectAll('.node');
 
         function ldist(d) {
-            return fcfg.linkDistance[d.class] || 150;
+            return 2 * 30;
+            //return fcfg.linkDistance[d.class] || 150;
         }
         function lstrg(d) {
-            return fcfg.linkStrength[d.class] || 1;
+            return 2 * 0.6;
+            //return fcfg.linkStrength[d.class] || 1;
         }
         function lchrg(d) {
             return fcfg.charge[d.class] || -200;
@@ -781,14 +981,11 @@
         }
 
         function atDragEnd(d, self) {
-            // once we've finished moving, pin the node in position,
-            // if it is a device (not a host)
-            if (d.class === 'device') {
-                d.fixed = true;
-                d3.select(self).classed('fixed', true);
-                if (config.useLiveData) {
-                    tellServerCoords(d);
-                }
+            // once we've finished moving, pin the node in position
+            d.fixed = true;
+            d3.select(self).classed('fixed', true);
+            if (config.useLiveData) {
+                tellServerCoords(d);
             }
         }
 
@@ -806,17 +1003,34 @@
             .size(forceDim)
             .nodes(network.nodes)
             .links(network.links)
-            .charge(lchrg)
+            .gravity(0.3)
+            .charge(-15000)
+            .friction(0.1)
+            //.charge(lchrg)
             .linkDistance(ldist)
             .linkStrength(lstrg)
             .on('tick', tick);
 
+            // TVUE
+            //.gravity(0.3)
+            //.charge(-15000)
+            //.friction(0.1)
+            //.linkDistance(function(d) { return d.value * 30; })
+            //.linkStrength(function(d) { return d.value * 0.6; })
+            //.size([w, h])
+            //.start();
+
         network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
     }
 
-    function load(view, ctx) {
+    function load(view, ctx, flags) {
         // cache the view token, so network topo functions can access it
         network.view = view;
+        config.useLiveData = !flags.local;
+
+        if (!config.useLiveData) {
+            prepareScenario(view, ctx, flags.debug);
+        }
 
         // set our radio buttons and key bindings
         view.setRadio(btnSet);