Added webservices forwarding script for UI
diff --git a/web/js/main.js b/web/js/main.js
new file mode 100644
index 0000000..9fb6f66
--- /dev/null
+++ b/web/js/main.js
@@ -0,0 +1,130 @@
+/*
+   Copyright 2012 IBM
+
+   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.
+*/
+
+var hackBase = "http://localhost:9000"; // put a URL here to access a different REST server
+
+var AppRouter = Backbone.Router.extend({
+
+    routes:{
+        "":"home",
+        "topology":"topology",
+        "switches":"switchList",
+        "switch/:id":"switchDetails",
+        "switch/:id/port/:p":"portDetails", // not clear if needed
+        "hosts":"hostList",
+        "host/:id":"hostDetails",
+        // "vlans":"vlanList" // maybe one day
+        // "vlan/:id":"vlanDetails"
+    },
+
+    initialize:function () {
+        this.headerView = new HeaderView();
+        $('.header').html(this.headerView.render().el);
+
+        // Close the search dropdown on click anywhere in the UI
+        $('body').click(function () {
+            $('.dropdown').removeClass("open");
+        });
+    },
+
+    home:function () {
+        $('#content').html(new HomeView().render().el);
+        $('ul[class="nav"] > li').removeClass('active');
+        $('a[href="/"]').parent().addClass('active');
+    },
+
+    topology:function () {
+        //console.log("switching to topology view");
+        var topo = new Topology();
+        $('#content').html(new TopologyView({model:topo, hosts:hl}).render().el);
+        // TODO factor this code out
+        $('ul.nav > li').removeClass('active');
+        $('li > a[href*="topology"]').parent().addClass('active');
+    },
+    
+    switchDetails:function (id) {
+        //console.log("switching [sic] to single switch view");
+        var sw = swl.get(id);
+        $('#content').html(new SwitchView({model:sw}).render().el);
+        $('ul.nav > li').removeClass('active');
+        $('li > a[href*="/switches"]').parent().addClass('active');
+    },
+    
+    switchList:function () {
+        //console.log("switching [sic] to switch list view");
+        $('#content').html(new SwitchListView({model:swl}).render().el);
+        $('ul.nav > li').removeClass('active');
+        $('li > a[href*="/switches"]').parent().addClass('active');
+    },
+
+    hostDetails:function (id) {
+        //console.log("switching to single host view");
+        var h = hl.get(id);
+        $('#content').html(new HostView({model:h}).render().el);
+        $('ul.nav > li').removeClass('active');
+        $('li > a[href*="/hosts"]').parent().addClass('active');
+    },
+    
+    hostList:function () {
+        //console.log("switching to host list view");
+        $('#content').html(new HostListView({model:hl}).render().el);
+        $('ul.nav > li').removeClass('active');
+        $('li > a[href*="/hosts"]').parent().addClass('active');
+    },
+
+});
+
+// load global models and reuse them
+var swl = new SwitchCollection();
+var hl  = new HostCollection();
+
+var updating = true;
+
+tpl.loadTemplates(['home', 'status', 'topology', 'header', 'switch', 'switch-list', 'switch-list-item', 'host', 'host-list', 'host-list-item', 'port-list', 'port-list-item', 'flow-list', 'flow-list-item'],
+    function () {
+        app = new AppRouter();
+        Backbone.history.start({pushState: true});
+        //console.log("started history")
+        
+        $(document).ready(function () {
+            // trigger Backbone routing when clicking on links, thanks to Atinux and pbnv
+            app.navigate("", true);
+
+            window.document.addEventListener('click', function(e) {
+                e = e || window.event
+                var target = e.target || e.srcElement
+                if ( target.nodeName.toLowerCase() === 'a' ) {
+                    e.preventDefault()
+                    var uri = target.getAttribute('href')
+                    app.navigate(uri.substr(1), true)
+                }
+            });
+            window.addEventListener('popstate', function(e) {
+                app.navigate(location.pathname.substr(1), true);
+            });
+            
+            // wait for the page to be rendered before loading any data
+            swl.fetch();
+            hl.fetch();
+            
+            setInterval(function () {
+                if(updating) {
+                    swl.fetch();
+                    hl.fetch();
+                }
+            }, 3000);
+        });
+    });
diff --git a/web/js/models/flowmodel.js b/web/js/models/flowmodel.js
new file mode 100644
index 0000000..80777c3
--- /dev/null
+++ b/web/js/models/flowmodel.js
@@ -0,0 +1,37 @@
+/*
+   Copyright 2012 IBM
+
+   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.
+*/
+
+window.Flow = Backbone.Model.extend({
+
+    defaults: {
+        receiveBytes: 0,
+        receivePackets: 0,
+        transmitBytes: 0,
+        transmitPackets: 0,
+    },
+
+    // initialize:function () {}
+
+});
+
+window.FlowCollection = Backbone.Collection.extend({
+
+    model:Flow,
+    
+    // instead of the collection loading its children, the switch will load them
+    // initialize:function () {}
+    
+});
\ No newline at end of file
diff --git a/web/js/models/hostmodel.js b/web/js/models/hostmodel.js
new file mode 100644
index 0000000..8de3dd6
--- /dev/null
+++ b/web/js/models/hostmodel.js
@@ -0,0 +1,77 @@
+/*
+   Copyright 2012 IBM
+
+   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.
+*/
+
+window.Host = Backbone.Model.extend({
+
+    defaults: {
+        // vlan: -1,
+        lastSeen: 'never',
+        ip: ' ',
+        swport: ' ',
+    },
+
+    // initialize:function () {}
+
+});
+
+window.HostCollection = Backbone.Collection.extend({
+
+    model:Host,
+
+    fetch:function () {
+        var self = this;
+        //console.log("fetching host list")
+        $.ajax({
+            url:hackBase + "/wm/device/",
+            dataType:"json",
+            success:function (data) {
+                //console.log("fetched  host list: " + data.length);
+                // console.log(data);
+                // data is a list of device hashes
+                var old_ids = self.pluck('id');
+                //console.log("old_ids" + old_ids);
+                _.each(data, function(h) {
+                    h.id = h.mac[0];
+                    old_ids = _.without(old_ids, h.id);
+                    if (h['attachmentPoint'].length > 0) {
+                        h.swport = _.reduce(h['attachmentPoint'], function(memo, ap) {
+                            return memo + ap.switchDPID + "-" + ap.port + " "}, "");
+                        //console.log(h.swport);
+                        h.lastSeen = new Date(h.lastSeen).toLocaleString();
+                        self.add(h, {silent: true});
+                    }
+                });
+                // old_ids now holds hosts that no longer exist; remove them
+                //console.log("old_ids" + old_ids);
+                _.each(old_ids, function(h) {
+                    console.log("---removing host " + h);
+                    self.remove({id:h});
+                });
+                self.trigger('add'); // batch redraws
+            }
+        });
+
+    },
+
+    /*
+     * findByName:function (key) { // TODO: Modify service to include firstName
+     * in search var url = (key == '') ? '/host/' : "/host/search/" + key;
+     * console.log('findByName: ' + key); var self = this; $.ajax({ url:url,
+     * dataType:"json", success:function (data) { console.log("search success: " +
+     * data.length); self.reset(data); } }); }
+     */
+
+});
diff --git a/web/js/models/portmodel.js b/web/js/models/portmodel.js
new file mode 100644
index 0000000..563f334
--- /dev/null
+++ b/web/js/models/portmodel.js
@@ -0,0 +1,42 @@
+/*
+   Copyright 2012 IBM
+
+   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.
+*/
+
+window.Port = Backbone.Model.extend({
+
+    defaults: {
+        name: '',
+        receiveBytes: 0,
+        receivePackets: 0,
+        transmitBytes: 0,
+        transmitPackets: 0,
+        dropped: 0,
+        errors: 0,
+    },
+
+    initialize:function () {
+        // TODO hook up associated hosts
+    }
+
+});
+
+window.PortCollection = Backbone.Collection.extend({
+
+    model:Port,
+    
+    // instead of the collection loading its children, the switch will load them
+    initialize:function () {}
+    
+});
\ No newline at end of file
diff --git a/web/js/models/statusmodel.js b/web/js/models/statusmodel.js
new file mode 100644
index 0000000..b7cdebd
--- /dev/null
+++ b/web/js/models/statusmodel.js
@@ -0,0 +1,73 @@
+/*
+   Copyright 2012 IBM
+
+   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.
+*/
+
+window.Status = Backbone.Model.extend({    
+    defaults: {
+        host: 'localhost',
+        ofport: 6633,
+        uptime: 'unknown',
+        free: 0,
+        total: 0,
+        healthy: 'unknown',
+        modules: [],
+        moduleText: ''
+    },
+    
+    initialize:function () {
+        var self = this;
+        console.log("fetching controller status");
+        $.ajax({
+            url:hackBase + "/wm/core/health/json",
+            dataType:"json",
+            success:function (data) {
+                console.log("fetched controller status: health");
+                self.set(data);
+                // console.log(self.toJSON());
+            }
+        });
+        $.ajax({
+            url:hackBase + "/wm/core/system/uptime/json",
+            dataType:"json",
+            success:function (data) {
+                console.log("fetched controller status: uptime");
+                self.set({uptime:(Math.round(data.systemUptimeMsec / 1000) + ' s')});
+                // console.log(self.toJSON());
+            }
+        });
+        $.ajax({
+            url:hackBase + "/wm/core/memory/json",
+            dataType:"json",
+            success:function (data) {
+                console.log("fetched controller status: memory");
+                self.set(data);
+                // console.log(self.toJSON());
+            }
+        });
+        $.ajax({
+            url:hackBase + "/wm/core/module/loaded/json",
+            dataType:"json",
+            success:function (data) {
+                console.log("fetched controller status: modules loaded");
+                // console.log(data);
+                self.set({modules:_.keys(data)});
+                self.set({moduleText:_.reduce(_.keys(data), function(s, m)
+                    {return s+m.replace("net.floodlightcontroller", "n.f")+", "}, '')});
+            }
+        });
+
+    }
+
+});
\ No newline at end of file
diff --git a/web/js/models/switchmodel.js b/web/js/models/switchmodel.js
new file mode 100644
index 0000000..4104dd0
--- /dev/null
+++ b/web/js/models/switchmodel.js
@@ -0,0 +1,295 @@
+/*
+   Copyright 2012 IBM
+
+   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.
+*/
+
+window.Switch = Backbone.Model.extend({
+
+    urlRoot:"/wm/core/switch/",
+    
+    defaults: {
+        datapathDescription: '',
+        hardwareDescription: '',
+        manufacturerDescription: '',
+        serialNumber: '',
+        softwareDescription: '',
+        flowCount: ' ',
+        packetCount: ' ',
+        byteCount: ' ',
+    },
+
+    initialize:function () {
+        var self = this;
+
+        //console.log("fetching switch " + this.id + " desc")
+        $.ajax({
+            url:hackBase + "/wm/core/switch/" + self.id + '/desc/json',
+            dataType:"json",
+            success:function (data) {
+                //console.log("fetched  switch " + self.id + " desc");
+                //console.log(data[self.id][0]);
+                self.set(data[self.id][0]);
+            }
+        });
+
+        //console.log("fetching switch " + this.id + " aggregate")
+        $.ajax({
+            url:hackBase + "/wm/core/switch/" + self.id + '/aggregate/json',
+            dataType:"json",
+            success:function (data) {
+                //console.log("fetched  switch " + self.id + " aggregate");
+                //console.log(data[self.id][0]);
+                self.set(data[self.id][0]);
+            }
+        });
+        self.trigger('add');
+        this.ports = new PortCollection();
+        this.flows = new FlowCollection();
+        //this.loadPorts();
+        //this.loadFlows();
+    },
+
+    fetch:function () {
+        this.initialize()
+    },
+
+    loadPorts:function () {
+        var self = this;
+        //console.log("fetching switch " + this.id + " ports")
+        //console.log("fetching switch " + this.id + " features")
+        $.when($.ajax({
+            url:hackBase + "/wm/core/switch/" + self.id + '/port/json',
+            dataType:"json",
+            success:function (data) {
+                //console.log("fetched  switch " + self.id + " ports");
+                //console.log(data[self.id]);
+                var old_ids = self.ports.pluck('id');
+                //console.log("old_ids" + old_ids);
+
+                // create port models
+                _.each(data[self.id], function(p) {
+                    // workaround for REST serialization signed/unsigned bug
+                    if(p.portNumber < 0) {p.portNumber = 65536 + p.portNumber};
+                    
+                    p.id = self.id+'-'+p.portNumber;
+                    old_ids = _.without(old_ids, p.id);
+                    p.dropped = p.receiveDropped + p.transmitDropped;
+                    p.errors = p.receiveCRCErrors + p.receiveErrors + p.receiveOverrunErrors +
+                        p.receiveFrameErrors + p.transmitErrors;
+                    // this is a knda kludgy way to merge models
+                    var m = self.ports.get(p.id);
+                    if(m) {
+                        m.set(p, {silent: true});
+                    } else {
+                        self.ports.add(p, {silent: true});
+                    }
+                    //console.log(p);
+                });
+                
+                // old_ids now holds ports that no longer exist; remove them
+                //console.log("old_ids" + old_ids);
+                _.each(old_ids, function(p) {
+                    console.log("removing port " + p);
+                    self.remove({id:p});
+                });
+            }
+        }),
+        $.ajax({
+            url:hackBase + "/wm/core/switch/" + self.id + '/features/json',
+            dataType:"json",
+            success:function (data) {
+                //console.log("fetched  switch " + self.id + " features");
+                //console.log(data[self.id]);
+                // update port models
+                _.each(data[self.id].ports, function(p) {
+                    p.id = self.id+'-'+p.portNumber;
+                    if(p.name != p.portNumber) {
+                        p.name = p.portNumber + ' (' + p.name + ')';
+                    }
+                    p.status = '';
+                    p.status += (p.state & 1) ? 'DOWN' : 'UP';
+                    switch(p.currentFeatures & 0x7f) {
+                    case 1:
+                        p.status += ' 10 Mbps';
+                        break;
+                    case 2:
+                        p.status += ' 10 Mbps FDX';
+                        break;
+                    case 4:
+                        p.status += ' 100 Mbps';
+                        break;
+                    case 8:
+                        p.status += ' 100 Mbps FDX';
+                        break;
+                    case 16:
+                        p.status += ' 1 Gbps'; // RLY?
+                        break;
+                    case 32:
+                        p.status += ' 1 Gbps FDX';
+                        break;
+                    case 64:
+                        p.status += ' 10 Gbps FDX';
+                        break;
+                    }
+                    // TODO parse copper/fiber, autoneg, pause
+                    
+                    // this is a knda kludgy way to merge models
+                    var m = self.ports.get(p.id);
+                    if(m) {
+                        m.set(p, {silent: true});
+                    } else {
+                        self.ports.add(p, {silent: true});
+                    }
+                    //console.log(p);
+                });
+            }
+        })).done(function() {
+            self.ports.trigger('add'); // batch redraws
+        });
+    },
+    
+    loadFlows:function () {
+        var self = this;
+        //console.log("fetching switch " + this.id + " flows")
+        $.ajax({
+            url:hackBase + "/wm/core/switch/" + self.id + '/flow/json',
+            dataType:"json",
+            success:function (data) {
+                //console.log("fetched  switch " + self.id + " flows");
+                var flows = data[self.id];
+                //console.log(flows);
+
+                // create flow models
+                var i = 0;
+                _.each(flows, function(f) {
+                    f.id = self.id + '-' + i++;
+
+                    // build human-readable match
+                    f.matchHTML = '';
+                    if(!(f.match.wildcards & (1<<0))) { // input port
+                        f.matchHTML += "port=" + f.match.inputPort + ", ";
+                    }
+                    if(!(f.match.wildcards & (1<<1))) { // VLAN ID
+                        f.matchHTML += "VLAN=" + f.match.dataLayerVirtualLan + ", ";
+                    }
+                    if(!(f.match.wildcards & (1<<20))) { // VLAN prio
+                        f.matchHTML += "prio=" + f.match.dataLayerVirtualLanPriorityCodePoint  + ", ";
+                    }
+                    if(!(f.match.wildcards & (1<<2))) { // src MAC
+                        f.matchHTML += "src=<a href='/host/" + f.match.dataLayerSource + "'>" +
+                        f.match.dataLayerSource + "</a>, ";
+                    }
+                    if(!(f.match.wildcards & (1<<3))) { // dest MAC
+                        f.matchHTML += "dest=<a href='/host/" + f.match.dataLayerDestination + "'>" +
+                        f.match.dataLayerDestination + "</a>, ";
+                    }
+                    if(!(f.match.wildcards & (1<<4))) { // Ethertype
+                        // TODO print a human-readable name instead of hex
+                        f.matchHTML += "ethertype=" + f.match.dataLayerType + ", ";
+                    }
+                    if(!(f.match.wildcards & (1<<5))) { // IP protocol
+                        // TODO print a human-readable name
+                        f.matchHTML += "proto=" + f.match.networkProtocol + ", ";
+                    }
+                    if(!(f.match.wildcards & (1<<6))) { // TCP/UDP source port
+                        f.matchHTML += "IP src port=" + f.match.transportSource + ", ";
+                    }
+                    if(!(f.match.wildcards & (1<<7))) { // TCP/UDP dest port
+                        f.matchHTML += "IP dest port=" + f.match.transportDestination  + ", ";
+                    }
+                    if(!(f.match.wildcards & (32<<8))) { // src IP
+                        f.matchHTML += "src=" + f.match.networkSource  + ", ";
+                    }
+                    if(!(f.match.wildcards & (32<<14))) { // dest IP
+                        f.matchHTML += "dest=" + f.match.networkDestination  + ", ";
+                    }
+                    if(!(f.match.wildcards & (1<<21))) { // IP TOS
+                        f.matchHTML += "TOS=" + f.match.networkTypeOfService  + ", ";
+                    }
+                    // remove trailing ", "
+                    f.matchHTML = f.matchHTML.substr(0, f.matchHTML.length - 2);
+
+                    // build human-readable action list
+                    f.actionText = _.reduce(f.actions, function (memo, a) {
+                        switch (a.type) {
+                            case "OUTPUT":
+                                return memo + "output " + a.port + ', ';
+                            case "OPAQUE_ENQUEUE":
+                                return memo + "enqueue " + a.port + ':' + a.queueId +  ', ';
+                            case "STRIP_VLAN":
+                                return memo + "strip VLAN, ";
+                            case "SET_VLAN_ID":
+                                return memo + "VLAN=" + a.virtualLanIdentifier + ', ';
+                            case "SET_VLAN_PCP":
+                                return memo + "prio=" + a.virtualLanPriorityCodePoint + ', ';
+                            case "SET_DL_SRC":
+                                return memo + "src=" + a.dataLayerAddress + ', ';
+                            case "SET_DL_DST":
+                                return memo + "dest=" + a.dataLayerAddress + ', ';
+                            case "SET_NW_TOS":
+                                return memo + "TOS=" + a.networkTypeOfService + ', ';
+                            case "SET_NW_SRC":
+                                return memo + "src=" + a.networkAddress + ', ';
+                            case "SET_NW_DST":
+                                return memo + "dest=" + a.networkAddress + ', ';
+                            case "SET_TP_SRC":
+                                return memo + "src port=" + a.transportPort + ', ';
+                            case "SET_TP_DST":
+                                return memo + "dest port=" + a.transportPort + ', ';
+                        }
+                    }, "");
+                    // remove trailing ", "
+                    f.actionText = f.actionText.substr(0, f.actionText.length - 2);
+
+                    //console.log(f);
+                    self.flows.add(f, {silent: true});
+                });
+                self.flows.trigger('add');
+            }
+        });
+    },
+});
+
+window.SwitchCollection = Backbone.Collection.extend({
+
+    model:Switch,
+    
+    fetch:function () {
+        var self = this;
+        //console.log("fetching switch list")
+        $.ajax({
+            url:hackBase + "/wm/core/controller/switches/json",
+            dataType:"json",
+            success:function (data) {
+                //console.log("fetched  switch list: " + data.length);
+                //console.log(data);
+                var old_ids = self.pluck('id');
+                //console.log("old_ids" + old_ids);
+                
+                _.each(data, function(sw) {
+                    old_ids = _.without(old_ids, sw['dpid']);
+                    self.add({id: sw['dpid'], inetAddress: sw.inetAddress,
+                              connectedSince: new Date(sw.connectedSince).toLocaleString()})});
+                
+                // old_ids now holds switches that no longer exist; remove them
+                //console.log("old_ids" + old_ids);
+                _.each(old_ids, function(sw) {
+                    console.log("removing switch " + sw);
+                    self.remove({id:sw});
+                });
+            },
+        });
+    },
+
+});
diff --git a/web/js/models/topologymodel.js b/web/js/models/topologymodel.js
new file mode 100644
index 0000000..c5d8f9b
--- /dev/null
+++ b/web/js/models/topologymodel.js
@@ -0,0 +1,65 @@
+/*
+   Copyright 2012 IBM
+
+   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.
+*/
+
+window.Topology = Backbone.Model.extend({
+
+    url:"/wm/topology/links/json",
+    
+    defaults:{
+        nodes: [],
+        links: [],
+    },
+
+    initialize:function () {
+        var self = this;
+        console.log("fetching topology")
+        $.ajax({
+            url:hackBase + self.url,
+            dataType:"json",
+            success:function (data) {
+                console.log("fetched topology: " + data.length);
+                // console.log(data);
+                self.nodes = {};
+                self.links = [];
+
+                // step 1: build unique array of switch IDs
+                /* this doesn't work if there's only one switch,
+                   because there are no switch-switch links
+                _.each(data, function (l) {
+                    self.nodes[l['src-switch']] = true;
+                    self.nodes[l['dst-switch']] = true;
+                });
+                // console.log(self.nodes);
+                var nl = _.keys(self.nodes);
+                */
+                var nl = swl.pluck('id');
+                self.nodes = _.map(nl, function (n) {return {name:n}});
+
+                // step 2: build array of links in format D3 expects
+                _.each(data, function (l) {
+                    self.links.push({source:nl.indexOf(l['src-switch']),
+                                     target:nl.indexOf(l['dst-switch']),
+                                     value:10});
+                });
+                // console.log(self.nodes);
+                // console.log(self.links);
+                self.trigger('change');
+                //self.set(data);
+            }
+        });
+    }
+
+});
\ No newline at end of file
diff --git a/web/js/utils.js b/web/js/utils.js
new file mode 100644
index 0000000..c086e29
--- /dev/null
+++ b/web/js/utils.js
@@ -0,0 +1,36 @@
+// template loader from Christophe Coenraets
+tpl = {
+
+    // Hash of preloaded templates for the app
+    templates:{},
+
+    // Recursively pre-load all the templates for the app.
+    // This implementation should be changed in a production environment. All the template files should be
+    // concatenated in a single file.
+    loadTemplates:function (names, callback) {
+
+        var that = this;
+
+        var loadTemplate = function (index) {
+            var name = names[index];
+            console.log('Loading template: ' + name);
+            $.get('tpl/' + name + '.html', function (data) {
+                that.templates[name] = data;
+                index++;
+                if (index < names.length) {
+                    loadTemplate(index);
+                } else {
+                    callback();
+                }
+            });
+        }
+
+        loadTemplate(0);
+    },
+
+    // Get template by name from hash of preloaded templates
+    get:function (name) {
+        return this.templates[name];
+    }
+
+};
\ No newline at end of file
diff --git a/web/js/views/flow.js b/web/js/views/flow.js
new file mode 100644
index 0000000..65e0b71
--- /dev/null
+++ b/web/js/views/flow.js
@@ -0,0 +1,69 @@
+/*
+   Copyright 2012 IBM
+
+   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.
+*/
+
+// not used for now
+window.FlowView = Backbone.View.extend({
+
+    initialize:function () {
+        this.template = _.template(tpl.get('flow'));
+        this.model.bind("change", this.render, this);
+    },
+
+    render:function (eventName) {
+        $(this.el).html(this.template(this.model.toJSON()));
+        return this;
+    }
+
+});
+
+window.FlowListItemView = Backbone.View.extend({
+
+    tagName:"tr",
+
+    initialize:function () {
+        this.template = _.template(tpl.get('flow-list-item'));
+        this.model.bind("change", this.render, this);
+    },
+
+    render:function (eventName) {
+        $(this.el).html(this.template(this.model.toJSON()));
+        return this;
+    }
+
+});
+
+// TODO throughput (bps) and pps sparklines would be nice here
+// TODO hovering over a MAC address could show a compact view of that host
+window.FlowListView = Backbone.View.extend({
+
+    initialize:function () {
+        this.template = _.template(tpl.get('flow-list'));
+        this.model.bind("change", this.render, this);
+        this.model.bind("add", this.render, this);
+    },
+
+    render:function (eventName) {
+        // console.log("rendering flow list view: " + this.model.models.length);
+        $(this.el).html(this.template({nflows:this.model.length}));
+        _.each(this.model.models, function (f) {
+            $(this.el).find('table.flow-table > tbody')
+                .append(new FlowListItemView({model:f}).render().el);
+        }, this);
+        return this;
+    },
+
+});
+
diff --git a/web/js/views/header.js b/web/js/views/header.js
new file mode 100644
index 0000000..65e49dd
--- /dev/null
+++ b/web/js/views/header.js
@@ -0,0 +1,49 @@
+/*
+   Copyright 2012 IBM
+
+   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.
+*/
+
+window.HeaderView = Backbone.View.extend({
+
+    initialize:function () {
+        this.template = _.template(tpl.get('header'));
+        // this.searchResults = new HostCollection();
+        // this.searchresultsView = new SearchListView({model:this.searchResults, className:'dropdown-menu'});
+    },
+
+    render:function (eventName) {
+        $(this.el).html(this.template());
+        $('#live-updates', this.el).change(function () {
+            updating = $(this).is(':checked');
+        })
+        // $('.navbar-search', this.el).append(this.searchresultsView.render().el);
+        return this;
+    },
+
+    events:{
+        "keyup .search-query":"search"
+    },
+
+    search:function (event) {
+//        var key = event.target.value;
+        var key = $('#searchText').val();
+        console.log('search ' + key);
+        // TODO search the host and switch lists
+        this.searchResults.findByName(key);
+        setTimeout(function () {
+            $('#searchForm').addClass('open');
+        });
+    }
+
+});
\ No newline at end of file
diff --git a/web/js/views/home.js b/web/js/views/home.js
new file mode 100644
index 0000000..0283002
--- /dev/null
+++ b/web/js/views/home.js
@@ -0,0 +1,41 @@
+/*
+   Copyright 2012 IBM
+
+   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.
+*/
+
+window.HomeView = Backbone.View.extend({
+
+    initialize:function () {
+        // console.log('Initializing Home View');
+        this.template = _.template(tpl.get('home'));
+    },
+
+    events:{
+        "click #showMeBtn":"showMeBtnClick"
+    },
+
+    render:function (eventName) {
+        $(this.el).html(this.template());
+        var stats = new Status();
+        $(this.el).find('#controller-status').html(new StatusView({model:stats}).render().el);
+        $(this.el).find('#switch-list').html(new SwitchListView({model:swl}).render().el);
+        $(this.el).find('#host-list').html(new HostListView({model:hl}).render().el);
+        return this;
+    },
+
+    showMeBtnClick:function () {
+        app.headerView.search();
+    }
+
+});
\ No newline at end of file
diff --git a/web/js/views/host.js b/web/js/views/host.js
new file mode 100644
index 0000000..705703a
--- /dev/null
+++ b/web/js/views/host.js
@@ -0,0 +1,67 @@
+/*
+   Copyright 2012 IBM
+
+   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.
+*/
+
+window.HostView = Backbone.View.extend({
+
+    initialize:function () {
+        this.template = _.template(tpl.get('host'));
+        this.model.bind("change", this.render, this);
+        this.model.bind("add", this.render, this);
+    },
+
+    render:function (eventName) {
+        $(this.el).html(this.template(this.model.toJSON()));
+        return this;
+    }
+
+});
+
+window.HostListView = Backbone.View.extend({
+
+    initialize:function () {
+        var self = this;
+        this.template = _.template(tpl.get('host-list'));
+        this.model.bind("change", this.render, this);
+        this.model.bind("add", this.render, this);
+        this.model.bind("remove", this.render, this);
+    },
+
+    render:function (eventName) {
+        $(this.el).html(this.template({nhosts:hl.length}));
+        _.each(this.model.models, function (h) {
+            $(this.el).find('table.host-table > tbody')
+                .append(new HostListItemView({model:h}).render().el);
+        }, this);
+        return this;
+    }
+});
+
+window.HostListItemView = Backbone.View.extend({
+
+    tagName:"tr",
+
+    initialize:function () {
+        this.template = _.template(tpl.get('host-list-item'));
+        this.model.bind("change", this.render, this);
+        this.model.bind("destroy", this.close, this);
+    },
+
+    render:function (eventName) {
+        $(this.el).html(this.template(this.model.toJSON()));
+        return this;
+    }
+
+});
\ No newline at end of file
diff --git a/web/js/views/port.js b/web/js/views/port.js
new file mode 100644
index 0000000..e9aadb9
--- /dev/null
+++ b/web/js/views/port.js
@@ -0,0 +1,70 @@
+/*
+   Copyright 2012 IBM
+
+   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.
+*/
+
+// not used for now
+window.PortView = Backbone.View.extend({
+
+    initialize:function () {
+        this.template = _.template(tpl.get('port'));
+        this.model.bind("change", this.render, this);
+        //this.model.bind("destroy", this.close, this);
+    },
+
+    render:function (eventName) {
+        $(this.el).html(this.template(this.model.toJSON()));
+        return this;
+    }
+
+});
+
+window.PortListItemView = Backbone.View.extend({
+
+    tagName:"tr",
+
+    initialize:function () {
+        this.template = _.template(tpl.get('port-list-item'));
+        this.model.bind("change", this.render, this);
+        //this.model.bind("destroy", this.close, this);
+    },
+
+    render:function (eventName) {
+        $(this.el).html(this.template(this.model.toJSON()));
+        return this;
+    }
+
+});
+
+// TODO throughput sparklines would be nice here
+window.PortListView = Backbone.View.extend({
+
+    initialize:function () {
+        this.template = _.template(tpl.get('port-list'));
+        this.model.bind("change", this.render, this);
+        this.model.bind("add", this.render, this);
+    },
+
+    render:function (eventName) {
+        // console.log("rendering port list view");
+        $(this.el).html(this.template({nports:this.model.length}));
+        _.each(this.model.models, function (p) {
+            $(this.el).find('table.port-table > tbody')
+                .append(new PortListItemView({model:p}).render().el);
+        }, this);
+        return this;
+    },
+
+});
+
diff --git a/web/js/views/status.js b/web/js/views/status.js
new file mode 100644
index 0000000..52c6c1c
--- /dev/null
+++ b/web/js/views/status.js
@@ -0,0 +1,30 @@
+/*
+   Copyright 2012 IBM
+
+   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.
+*/
+
+window.StatusView = Backbone.View.extend({
+
+    initialize:function () {
+        this.template = _.template(tpl.get('status'));
+        this.model.bind("change", this.render, this);
+    },
+
+    render:function (eventName) {
+        // console.log("rendering status");
+        $(this.el).html(this.template(this.model.toJSON()));
+        //$(this.el).html(this.template());
+        return this;
+    }
+});
\ No newline at end of file
diff --git a/web/js/views/switch.js b/web/js/views/switch.js
new file mode 100644
index 0000000..d457633
--- /dev/null
+++ b/web/js/views/switch.js
@@ -0,0 +1,73 @@
+/*
+   Copyright 2012 IBM
+
+   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.
+*/
+
+window.SwitchView = Backbone.View.extend({
+
+    initialize:function () {
+        this.template = _.template(tpl.get('switch'));
+        this.model.bind("change", this.render, this);
+        //this.model.bind("destroy", this.close, this);
+        
+        // some parts of the model are large and are only needed in detail view
+        this.model.loadPorts();
+        this.model.loadFlows();
+    },
+
+    render:function (eventName) {
+        $(this.el).html(this.template(this.model.toJSON()));
+        $(this.el).find('#port-list').html(new PortListView({model:this.model.ports}).render().el);
+        $(this.el).find('#flow-list').html(new FlowListView({model:this.model.flows}).render().el);
+        return this;
+    }
+
+});
+
+window.SwitchListItemView = Backbone.View.extend({
+
+    tagName:"tr",
+
+    initialize:function () {
+        this.template = _.template(tpl.get('switch-list-item'));
+        this.model.bind("change", this.render, this);
+    //this.model.bind("destroy", this.close, this);
+    },
+
+    render:function (eventName) {
+        $(this.el).html(this.template(this.model.toJSON()));
+        return this;
+    }
+
+});
+
+window.SwitchListView = Backbone.View.extend({
+
+    initialize:function () {
+        this.template = _.template(tpl.get('switch-list'));
+        this.model.bind("change", this.render, this);
+        this.model.bind("remove", this.render, this);
+    },
+
+    render:function (eventName) {
+        $(this.el).html(this.template({nswitches:swl.length}));
+        _.each(this.model.models, function (sw) {
+            $(this.el).find('table.switch-table > tbody')
+                .append(new SwitchListItemView({model:sw}).render().el);
+        }, this);
+        return this;
+    },
+
+});
+
diff --git a/web/js/views/topology.js b/web/js/views/topology.js
new file mode 100644
index 0000000..77129aa
--- /dev/null
+++ b/web/js/views/topology.js
@@ -0,0 +1,111 @@
+/*
+   Copyright 2012 IBM
+
+   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.
+*/
+
+window.TopologyView = Backbone.View.extend({
+    initialize:function () {
+        this.template = _.template(tpl.get('topology'));
+        this.model.bind("change", this.render, this);
+        this.hosts = this.options.hosts.models;
+        this.host_links = [];
+    },
+
+    render:function (eventName) {
+        $(this.el).html(this.template());
+        var width = 900,
+            height = 600;
+        var color = d3.scale.category20();
+        var force = d3.layout.force()
+                             .charge(-500)
+                             .linkDistance(200)
+                             .size([width, height]);
+        var svg = d3.select("#topology-graph").append("svg")
+                    .attr("width", width)
+                    .attr("height", height);
+        if(this.model.nodes) {
+            for (var i = 0; i < this.model.nodes.length; i++) {
+                this.model.nodes[i].group = 1;
+                this.model.nodes[i].id = this.model.nodes[i].name;
+            }
+            
+            for (var i = 0; i < this.hosts.length; i++) {
+                host = this.hosts[i];
+                if (host.attributes['ipv4'].length > 0) {
+                    host.name = host.attributes['ipv4'][0] + "\n" + host.id;
+                } else {
+                    host.name = host.id;
+                }
+                host.group = 2;
+                //console.log(host);
+            }
+            
+            var all_nodes = this.model.nodes.concat(this.hosts);
+            
+            var all_nodes_map = [];
+            
+            _.each(all_nodes, function(n) {
+                all_nodes_map[n.id] = n;
+            });
+            
+            for (var i = 0; i < this.hosts.length; i++) {
+                host = this.hosts[i];
+                //for (var j = 0; j < host.attributes['attachmentPoint'].length; j++) {
+                for (var j = 0; j < 1; j++) { // FIXME hack to ignore multiple APs
+                    var link = {source:all_nodes_map[host.id],
+                                target:all_nodes_map[host.attributes['attachmentPoint'][j]['switchDPID']],
+                                value:10};
+                    //console.log(link);
+                    if ( link.source && link.target) {
+                        this.host_links.push(link);
+                    } else {
+                        console.log("Error: skipping link with undefined stuff!")
+                    }
+                }
+            }
+            
+            var all_links = this.model.links.concat(this.host_links);
+            
+            force.nodes(all_nodes).links(all_links).start();
+            var link = svg.selectAll("line.link").data(all_links).enter()
+                          .append("line").attr("class", "link")
+                          .style("stroke", function (d) { return "black"; });
+            var node = svg.selectAll(".node").data(all_nodes)
+                          .enter().append("g")
+                          .attr("class", "node")
+                          .call(force.drag);
+                          
+            node.append("image")
+                .attr("xlink:href", function (d) {return d.group==1 ? "/ui/img/switch.png" : "/ui/img/server.png"})
+                .attr("x", -16).attr("y", -16)
+                .attr("width", 32).attr("height", 32);
+            node.append("text").attr("dx", 20).attr("dy", ".35em")
+                .text(function(d) { return d.name });
+            node.on("click", function (d) {
+                // TODO we could add some functionality here
+                console.log('clicked '+d.name);
+            });
+            node.append("title").text(function(d) { return d.name; });
+            force.on("tick", function() {
+                link.attr("x1", function(d) { return d.source.x; })
+                    .attr("y1", function(d) { return d.source.y; })
+                    .attr("x2", function(d) { return d.target.x; })
+                    .attr("y2", function(d) { return d.target.y; });
+                node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
+                
+            });
+        }
+        return this;
+    }
+});