[Pkg-mozext-commits] [lightbeam] 02/03: Imported Upstream version 1.0.10.2+dfsg

Dmitry Smirnov onlyjob at moszumanska.debian.org
Mon Apr 20 10:24:12 UTC 2015


This is an automated email from the git hooks/post-receive script.

onlyjob pushed a commit to branch upstream
in repository lightbeam.

commit b41173b
Author: Dmitry Smirnov <onlyjob at member.fsf.org>
Date:   Tue Nov 18 23:05:57 2014

    Imported Upstream version 1.0.10.2+dfsg
---
 bootstrap.js                                       |  339 ++++++
 defaults/preferences/prefs.js                      |    3 +
 harness-options.json                               |  235 ++++
 install.rdf                                        |   28 +
 locales.json                                       |    1 +
 options.xul                                        |   24 +
 resources/lightbeam/data/OpenSans.css              |   23 +
 resources/lightbeam/data/aggregate.js              |  523 ++++++++
 resources/lightbeam/data/content-script.js         |   43 +
 resources/lightbeam/data/dialog.js                 |  257 ++++
 resources/lightbeam/data/events.js                 |   47 +
 resources/lightbeam/data/first-run.css             |   63 +
 resources/lightbeam/data/first-run.html            |   56 +
 resources/lightbeam/data/font-awesome.css          |  537 +++++++++
 resources/lightbeam/data/graph.js                  |  416 +++++++
 .../lightbeam/data/icons/lightbeam_150x45.png      |  Bin 0 -> 3986 bytes
 .../lightbeam/data/icons/lightbeam_icon_block.png  |  Bin 0 -> 493 bytes
 .../data/icons/lightbeam_icon_empty_list.png       |  Bin 0 -> 328 bytes
 .../lightbeam/data/icons/lightbeam_icon_hide.png   |  Bin 0 -> 1060 bytes
 .../data/icons/lightbeam_icon_unblock.png          |  Bin 0 -> 437 bytes
 .../lightbeam/data/icons/lightbeam_icon_watch.png  |  Bin 0 -> 756 bytes
 .../data/icons/lightbeam_logo-only_16x16.png       |  Bin 0 -> 565 bytes
 .../data/icons/lightbeam_logo-only_32x32.png       |  Bin 0 -> 1138 bytes
 .../data/icons/lightbeam_logo-only_48x48.png       |  Bin 0 -> 913 bytes
 .../data/icons/lightbeam_logo-wordmark_300x150.png |  Bin 0 -> 8528 bytes
 .../data/image/Lightbeam---Wordmark-Beta.png       |  Bin 0 -> 3212 bytes
 .../lightbeam/data/image/Lightbeam_radio_off.png   |  Bin 0 -> 758 bytes
 .../lightbeam/data/image/Lightbeam_radio_on.png    |  Bin 0 -> 3012 bytes
 .../data/image/lightbeam__wordmark_temp.png        |  Bin 0 -> 2668 bytes
 .../lightbeam/data/image/lightbeam_icon_about.png  |  Bin 0 -> 312 bytes
 .../data/image/lightbeam_icon_checkbox.png         |  Bin 0 -> 396 bytes
 .../data/image/lightbeam_icon_download2.png        |  Bin 0 -> 369 bytes
 .../data/image/lightbeam_icon_feedback.png         |  Bin 0 -> 368 bytes
 .../lightbeam/data/image/lightbeam_icon_graph.png  |  Bin 0 -> 491 bytes
 .../lightbeam/data/image/lightbeam_icon_help.png   |  Bin 0 -> 410 bytes
 .../lightbeam/data/image/lightbeam_icon_list.png   |  Bin 0 -> 380 bytes
 .../data/image/lightbeam_icon_list_blue.png        |  Bin 0 -> 380 bytes
 .../lightbeam/data/image/lightbeam_icon_reset.png  |  Bin 0 -> 398 bytes
 .../lightbeam/data/image/lightbeam_icon_sortby.png |  Bin 0 -> 386 bytes
 .../data/image/lightbeam_icon_website.png          |  Bin 0 -> 584 bytes
 .../lightbeam/data/image/lightbeam_logo-only.svg   |  139 +++
 .../data/image/lightbeam_popup_blocked.png         |  Bin 0 -> 3533 bytes
 .../data/image/lightbeam_popup_hidden.png          |  Bin 0 -> 6149 bytes
 .../data/image/lightbeam_popup_privacy.png         |  Bin 0 -> 5697 bytes
 .../data/image/lightbeam_popup_stopsharing2.png    |  Bin 0 -> 2395 bytes
 .../data/image/lightbeam_popup_warningreset.png    |  Bin 0 -> 4746 bytes
 .../data/image/lightbeam_popup_warningsharing.png  |  Bin 0 -> 5001 bytes
 resources/lightbeam/data/index.html                |  479 ++++++++
 resources/lightbeam/data/infobar.js                |  362 ++++++
 resources/lightbeam/data/initialPage.js            |    6 +
 resources/lightbeam/data/lightbeam.js              |  178 +++
 resources/lightbeam/data/list.js                   |  709 +++++++++++
 resources/lightbeam/data/map.svg                   |   12 +
 resources/lightbeam/data/parseuri.js               |   32 +
 resources/lightbeam/data/picoModal/LICENSE.md      |   23 +
 .../data/picoModal/picoModal-1.0.0.min.js          |    1 +
 resources/lightbeam/data/style.css                 | 1265 ++++++++++++++++++++
 resources/lightbeam/data/svgdataset.js             |   51 +
 resources/lightbeam/data/tooltip.js                |  100 ++
 resources/lightbeam/data/ui.js                     |  516 ++++++++
 resources/lightbeam/data/upgrade.html              |   45 +
 resources/lightbeam/lib/connection.js              |  300 +++++
 resources/lightbeam/lib/main.js                    |   82 ++
 resources/lightbeam/lib/persist.js                 |  153 +++
 resources/lightbeam/lib/shared/menuitems.js        |  244 ++++
 resources/lightbeam/lib/shared/policy.js           |  155 +++
 resources/lightbeam/lib/shared/unload+.js          |   98 ++
 resources/lightbeam/lib/tab/events.js              |   20 +
 resources/lightbeam/lib/tab/utils.js               |  118 ++
 resources/lightbeam/lib/ui.js                      |  295 +++++
 70 files changed, 7978 insertions(+)

diff --git a/bootstrap.js b/bootstrap.js
new file mode 100644
index 0000000..d430d70
--- /dev/null
+++ b/bootstrap.js
@@ -0,0 +1,339 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// @see http://mxr.mozilla.org/mozilla-central/source/js/src/xpconnect/loader/mozJSComponentLoader.cpp
+
+'use strict';
+
+// IMPORTANT: Avoid adding any initialization tasks here, if you need to do
+// something before add-on is loaded consider addon/runner module instead!
+
+const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu,
+        results: Cr, manager: Cm } = Components;
+const ioService = Cc['@mozilla.org/network/io-service;1'].
+                  getService(Ci.nsIIOService);
+const resourceHandler = ioService.getProtocolHandler('resource').
+                        QueryInterface(Ci.nsIResProtocolHandler);
+const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')();
+const scriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1'].
+                     getService(Ci.mozIJSSubScriptLoader);
+const prefService = Cc['@mozilla.org/preferences-service;1'].
+                    getService(Ci.nsIPrefService).
+                    QueryInterface(Ci.nsIPrefBranch);
+const appInfo = Cc["@mozilla.org/xre/app-info;1"].
+                getService(Ci.nsIXULAppInfo);
+const vc = Cc["@mozilla.org/xpcom/version-comparator;1"].
+           getService(Ci.nsIVersionComparator);
+
+
+const REASON = [ 'unknown', 'startup', 'shutdown', 'enable', 'disable',
+                 'install', 'uninstall', 'upgrade', 'downgrade' ];
+
+const bind = Function.call.bind(Function.bind);
+
+let loader = null;
+let unload = null;
+let cuddlefishSandbox = null;
+let nukeTimer = null;
+
+// Utility function that synchronously reads local resource from the given
+// `uri` and returns content string.
+function readURI(uri) {
+  let ioservice = Cc['@mozilla.org/network/io-service;1'].
+    getService(Ci.nsIIOService);
+  let channel = ioservice.newChannel(uri, 'UTF-8', null);
+  let stream = channel.open();
+
+  let cstream = Cc['@mozilla.org/intl/converter-input-stream;1'].
+    createInstance(Ci.nsIConverterInputStream);
+  cstream.init(stream, 'UTF-8', 0, 0);
+
+  let str = {};
+  let data = '';
+  let read = 0;
+  do {
+    read = cstream.readString(0xffffffff, str);
+    data += str.value;
+  } while (read != 0);
+
+  cstream.close();
+
+  return data;
+}
+
+// We don't do anything on install & uninstall yet, but in a future
+// we should allow add-ons to cleanup after uninstall.
+function install(data, reason) {}
+function uninstall(data, reason) {}
+
+function startup(data, reasonCode) {
+  try {
+    let reason = REASON[reasonCode];
+    // URI for the root of the XPI file.
+    // 'jar:' URI if the addon is packed, 'file:' URI otherwise.
+    // (Used by l10n module in order to fetch `locale` folder)
+    let rootURI = data.resourceURI.spec;
+
+    // TODO: Maybe we should perform read harness-options.json asynchronously,
+    // since we can't do anything until 'sessionstore-windows-restored' anyway.
+    let options = JSON.parse(readURI(rootURI + './harness-options.json'));
+
+    let id = options.jetpackID;
+    let name = options.name;
+
+    // Clean the metadata
+    options.metadata[name]['permissions'] = options.metadata[name]['permissions'] || {};
+
+    // freeze the permissionss
+    Object.freeze(options.metadata[name]['permissions']);
+    // freeze the metadata
+    Object.freeze(options.metadata[name]);
+
+    // Register a new resource 'domain' for this addon which is mapping to
+    // XPI's `resources` folder.
+    // Generate the domain name by using jetpack ID, which is the extension ID
+    // by stripping common characters that doesn't work as a domain name:
+    let uuidRe =
+      /^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/;
+
+    let domain = id.
+      toLowerCase().
+      replace(/@/g, '-at-').
+      replace(/\./g, '-dot-').
+      replace(uuidRe, '$1');
+
+    let prefixURI = 'resource://' + domain + '/';
+    let resourcesURI = ioService.newURI(rootURI + '/resources/', null, null);
+    resourceHandler.setSubstitution(domain, resourcesURI);
+
+    // Create path to URLs mapping supported by loader.
+    let paths = {
+      // Relative modules resolve to add-on package lib
+      './': prefixURI + name + '/lib/',
+      './tests/': prefixURI + name + '/tests/',
+      '': 'resource://gre/modules/commonjs/'
+    };
+
+    // Maps addon lib and tests ressource folders for each package
+    paths = Object.keys(options.metadata).reduce(function(result, name) {
+      result[name + '/'] = prefixURI + name + '/lib/'
+      result[name + '/tests/'] = prefixURI + name + '/tests/'
+      return result;
+    }, paths);
+
+    // We need to map tests folder when we run sdk tests whose package name
+    // is stripped
+    if (name == 'addon-sdk')
+      paths['tests/'] = prefixURI + name + '/tests/';
+
+    let useBundledSDK = options['force-use-bundled-sdk'];
+    if (!useBundledSDK) {
+      try {
+        useBundledSDK = prefService.getBoolPref("extensions.addon-sdk.useBundledSDK");
+      }
+      catch (e) {
+        // Pref doesn't exist, allow using Firefox shipped SDK
+      }
+    }
+
+    // Starting with Firefox 21.0a1, we start using modules shipped into firefox
+    // Still allow using modules from the xpi if the manifest tell us to do so.
+    // And only try to look for sdk modules in xpi if the xpi actually ship them
+    if (options['is-sdk-bundled'] &&
+        (vc.compare(appInfo.version, '21.0a1') < 0 || useBundledSDK)) {
+      // Maps sdk module folders to their resource folder
+      paths[''] = prefixURI + 'addon-sdk/lib/';
+      // test.js is usually found in root commonjs or SDK_ROOT/lib/ folder,
+      // so that it isn't shipped in the xpi. Keep a copy of it in sdk/ folder
+      // until we no longer support SDK modules in XPI:
+      paths['test'] = prefixURI + 'addon-sdk/lib/sdk/test.js';
+    }
+
+    // Retrieve list of module folder overloads based on preferences in order to
+    // eventually used a local modules instead of files shipped into Firefox.
+    let branch = prefService.getBranch('extensions.modules.' + id + '.path');
+    paths = branch.getChildList('', {}).reduce(function (result, name) {
+      // Allows overloading of any sub folder by replacing . by / in pref name
+      let path = name.substr(1).split('.').join('/');
+      // Only accept overloading folder by ensuring always ending with `/`
+      if (path) path += '/';
+      let fileURI = branch.getCharPref(name);
+
+      // On mobile, file URI has to end with a `/` otherwise, setSubstitution
+      // takes the parent folder instead.
+      if (fileURI[fileURI.length-1] !== '/')
+        fileURI += '/';
+
+      // Maps the given file:// URI to a resource:// in order to avoid various
+      // failure that happens with file:// URI and be close to production env
+      let resourcesURI = ioService.newURI(fileURI, null, null);
+      let resName = 'extensions.modules.' + domain + '.commonjs.path' + name;
+      resourceHandler.setSubstitution(resName, resourcesURI);
+
+      result[path] = 'resource://' + resName + '/';
+      return result;
+    }, paths);
+
+    // Make version 2 of the manifest
+    let manifest = options.manifest;
+
+    // Import `cuddlefish.js` module using a Sandbox and bootstrap loader.
+    let cuddlefishPath = 'loader/cuddlefish.js';
+    let cuddlefishURI = 'resource://gre/modules/commonjs/sdk/' + cuddlefishPath;
+    if (paths['sdk/']) { // sdk folder has been overloaded
+                         // (from pref, or cuddlefish is still in the xpi)
+      cuddlefishURI = paths['sdk/'] + cuddlefishPath;
+    }
+    else if (paths['']) { // root modules folder has been overloaded
+      cuddlefishURI = paths[''] + 'sdk/' + cuddlefishPath;
+    }
+
+    cuddlefishSandbox = loadSandbox(cuddlefishURI);
+    let cuddlefish = cuddlefishSandbox.exports;
+
+    // Normalize `options.mainPath` so that it looks like one that will come
+    // in a new version of linker.
+    let main = options.mainPath;
+
+    unload = cuddlefish.unload;
+    loader = cuddlefish.Loader({
+      paths: paths,
+      // modules manifest.
+      manifest: manifest,
+
+      // Add-on ID used by different APIs as a unique identifier.
+      id: id,
+      // Add-on name.
+      name: name,
+      // Add-on version.
+      version: options.metadata[name].version,
+      // Add-on package descriptor.
+      metadata: options.metadata[name],
+      // Add-on load reason.
+      loadReason: reason,
+
+      prefixURI: prefixURI,
+      // Add-on URI.
+      rootURI: rootURI,
+      // options used by system module.
+      // File to write 'OK' or 'FAIL' (exit code emulation).
+      resultFile: options.resultFile,
+      // Arguments passed as --static-args
+      staticArgs: options.staticArgs,
+      // Add-on preferences branch name
+      preferencesBranch: options.preferencesBranch,
+
+      // Arguments related to test runner.
+      modules: {
+        '@test/options': {
+          allTestModules: options.allTestModules,
+          iterations: options.iterations,
+          filter: options.filter,
+          profileMemory: options.profileMemory,
+          stopOnError: options.stopOnError,
+          verbose: options.verbose,
+          parseable: options.parseable,
+          checkMemory: options.check_memory,
+        }
+      }
+    });
+
+    let module = cuddlefish.Module('sdk/loader/cuddlefish', cuddlefishURI);
+    let require = cuddlefish.Require(loader, module);
+
+    require('sdk/addon/runner').startup(reason, {
+      loader: loader,
+      main: main,
+      prefsURI: rootURI + 'defaults/preferences/prefs.js'
+    });
+  } catch (error) {
+    dump('Bootstrap error: ' +
+         (error.message ? error.message : String(error)) + '\n' +
+         (error.stack || error.fileName + ': ' + error.lineNumber) + '\n');
+    throw error;
+  }
+};
+
+function loadSandbox(uri) {
+  let proto = {
+    sandboxPrototype: {
+      loadSandbox: loadSandbox,
+      ChromeWorker: ChromeWorker
+    }
+  };
+  let sandbox = Cu.Sandbox(systemPrincipal, proto);
+  // Create a fake commonjs environnement just to enable loading loader.js
+  // correctly
+  sandbox.exports = {};
+  sandbox.module = { uri: uri, exports: sandbox.exports };
+  sandbox.require = function (id) {
+    if (id !== "chrome")
+      throw new Error("Bootstrap sandbox `require` method isn't implemented.");
+
+    return Object.freeze({ Cc: Cc, Ci: Ci, Cu: Cu, Cr: Cr, Cm: Cm,
+      CC: bind(CC, Components), components: Components,
+      ChromeWorker: ChromeWorker });
+  };
+  scriptLoader.loadSubScript(uri, sandbox, 'UTF-8');
+  return sandbox;
+}
+
+function unloadSandbox(sandbox) {
+  if ("nukeSandbox" in Cu)
+    Cu.nukeSandbox(sandbox);
+}
+
+function setTimeout(callback, delay) {
+  let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+  timer.initWithCallback({ notify: callback }, delay,
+                         Ci.nsITimer.TYPE_ONE_SHOT);
+  return timer;
+}
+
+function shutdown(data, reasonCode) {
+  let reason = REASON[reasonCode];
+  if (loader) {
+    unload(loader, reason);
+    unload = null;
+
+    // Don't waste time cleaning up if the application is shutting down
+    if (reason != "shutdown") {
+      // Avoid leaking all modules when something goes wrong with one particular
+      // module. Do not clean it up immediatly in order to allow executing some
+      // actions on addon disabling.
+      // We need to keep a reference to the timer, otherwise it is collected
+      // and won't ever fire.
+      nukeTimer = setTimeout(nukeModules, 1000);
+    }
+  }
+};
+
+function nukeModules() {
+  nukeTimer = null;
+  // module objects store `exports` which comes from sandboxes
+  // We should avoid keeping link to these object to avoid leaking sandboxes
+  for (let key in loader.modules) {
+    delete loader.modules[key];
+  }
+  // Direct links to sandboxes should be removed too
+  for (let key in loader.sandboxes) {
+    let sandbox = loader.sandboxes[key];
+    delete loader.sandboxes[key];
+    // Bug 775067: From FF17 we can kill all CCW from a given sandbox
+    unloadSandbox(sandbox);
+  }
+  loader = null;
+
+  // both `toolkit/loader` and `system/xul-app` are loaded as JSM's via
+  // `cuddlefish.js`, and needs to be unloaded to avoid memory leaks, when
+  // the addon is unload.
+
+  unloadSandbox(cuddlefishSandbox.loaderSandbox);
+  unloadSandbox(cuddlefishSandbox.xulappSandbox);
+
+  // Bug 764840: We need to unload cuddlefish otherwise it will stay alive
+  // and keep a reference to this compartment.
+  unloadSandbox(cuddlefishSandbox);
+  cuddlefishSandbox = null;
+}
diff --git a/defaults/preferences/prefs.js b/defaults/preferences/prefs.js
new file mode 100644
index 0000000..932a19b
--- /dev/null
+++ b/defaults/preferences/prefs.js
@@ -0,0 +1,3 @@
+pref("extensions.jid1-F9UJ2thwoAm5gQ at jetpack.contributeData", false);
+pref("extensions.jid1-F9UJ2thwoAm5gQ at jetpack.defaultVisualization", "graph");
+pref("extensions.jid1-F9UJ2thwoAm5gQ at jetpack.defaultFilter", "daily");
diff --git a/harness-options.json b/harness-options.json
new file mode 100644
index 0000000..2a1e39f
--- /dev/null
+++ b/harness-options.json
@@ -0,0 +1,235 @@
+{
+ "check_memory": false, 
+ "enable_e10s": false, 
+ "is-sdk-bundled": false, 
+ "jetpackID": "jid1-F9UJ2thwoAm5gQ at jetpack", 
+ "loader": "addon-sdk/lib/sdk/loader/cuddlefish.js", 
+ "main": "main", 
+ "mainPath": "lightbeam/main", 
+ "manifest": {
+  "lightbeam/connection": {
+   "docsSHA256": null, 
+   "jsSHA256": "7886c027d302e70900886e9eb4c2c142745fc890163c23535a2db29e376e2ba3", 
+   "moduleName": "connection", 
+   "packageName": "lightbeam", 
+   "requirements": {
+    "./persist": "lightbeam/persist", 
+    "./tab/utils": "lightbeam/tab/utils", 
+    "chrome": "chrome", 
+    "sdk/event/core": "sdk/event/core", 
+    "sdk/request": "sdk/request", 
+    "sdk/self": "sdk/self", 
+    "sdk/simple-storage": "sdk/simple-storage", 
+    "sdk/timers": "sdk/timers"
+   }, 
+   "sectionName": "lib"
+  }, 
+  "lightbeam/main": {
+   "docsSHA256": "efe0e4fb21cb5cda6cf17034aabd42097260ba5dac2083d1160e12dcde488f5f", 
+   "jsSHA256": "e1a65f7be45e2e6bd828c384845d4f2641df579525192b2f36e4361b54d731fd", 
+   "moduleName": "main", 
+   "packageName": "lightbeam", 
+   "requirements": {
+    "./connection": "lightbeam/connection", 
+    "./persist": "lightbeam/persist", 
+    "./tab/events": "lightbeam/tab/events", 
+    "./ui": "lightbeam/ui", 
+    "sdk/page-mod": "sdk/page-mod", 
+    "sdk/self": "sdk/self", 
+    "sdk/system/events": "sdk/system/events", 
+    "sdk/tabs": "sdk/tabs"
+   }, 
+   "sectionName": "lib"
+  }, 
+  "lightbeam/persist": {
+   "docsSHA256": null, 
+   "jsSHA256": "71dba92441163aa9577934ce83d947fdb80603510ff78f8b6fec843cd83ffcec", 
+   "moduleName": "persist", 
+   "packageName": "lightbeam", 
+   "requirements": {
+    "chrome": "chrome", 
+    "sdk/preferences/service": "sdk/preferences/service", 
+    "sdk/request": "sdk/request", 
+    "sdk/simple-prefs": "sdk/simple-prefs", 
+    "sdk/simple-storage": "sdk/simple-storage", 
+    "sdk/system/xul-app": "sdk/system/xul-app"
+   }, 
+   "sectionName": "lib"
+  }, 
+  "lightbeam/shared/menuitems": {
+   "docsSHA256": null, 
+   "jsSHA256": "0439dfbb105ef83c0f57d55519946079bf4b9b24691d825b76de4994ba13ab81", 
+   "moduleName": "shared/menuitems", 
+   "packageName": "lightbeam", 
+   "requirements": {
+    "./unload+": "lightbeam/shared/unload+", 
+    "sdk/core/heritage": "sdk/core/heritage", 
+    "sdk/core/namespace": "sdk/core/namespace", 
+    "sdk/deprecated/api-utils": "sdk/deprecated/api-utils", 
+    "sdk/deprecated/window-utils": "sdk/deprecated/window-utils", 
+    "sdk/event/core": "sdk/event/core", 
+    "sdk/event/target": "sdk/event/target", 
+    "sdk/window/utils": "sdk/window/utils"
+   }, 
+   "sectionName": "lib"
+  }, 
+  "lightbeam/shared/policy": {
+   "docsSHA256": null, 
+   "jsSHA256": "0ffd8914168d64cc4f6ce2779e671069e34d0fc5ae4d82d2206d3fb9b52088d0", 
+   "moduleName": "shared/policy", 
+   "packageName": "lightbeam", 
+   "requirements": {
+    "chrome": "chrome", 
+    "sdk/core/heritage": "sdk/core/heritage", 
+    "sdk/core/namespace": "sdk/core/namespace", 
+    "sdk/deprecated/api-utils": "sdk/deprecated/api-utils", 
+    "sdk/platform/xpcom": "sdk/platform/xpcom", 
+    "sdk/self": "sdk/self", 
+    "sdk/system/unload": "sdk/system/unload"
+   }, 
+   "sectionName": "lib"
+  }, 
+  "lightbeam/shared/unload+": {
+   "docsSHA256": null, 
+   "jsSHA256": "6e36c26b2cfbdf4dba923c7e7a384645487e9b435b6cd2ae61e2f91b0f90f789", 
+   "moduleName": "shared/unload+", 
+   "packageName": "lightbeam", 
+   "requirements": {
+    "sdk/core/heritage": "sdk/core/heritage", 
+    "sdk/core/namespace": "sdk/core/namespace", 
+    "sdk/system/unload": "sdk/system/unload"
+   }, 
+   "sectionName": "lib"
+  }, 
+  "lightbeam/tab/events": {
+   "docsSHA256": null, 
+   "jsSHA256": "7ac2b53840fa6075c83a66e5e45632df94b0adcba912dd107366164589ab2bfe", 
+   "moduleName": "tab/events", 
+   "packageName": "lightbeam", 
+   "requirements": {
+    "./utils": "lightbeam/tab/utils", 
+    "sdk/tabs": "sdk/tabs"
+   }, 
+   "sectionName": "lib"
+  }, 
+  "lightbeam/tab/utils": {
+   "docsSHA256": null, 
+   "jsSHA256": "926f08f5219618c18b763cf3e16364481c6ef33a30510618d412cb1916b54478", 
+   "moduleName": "tab/utils", 
+   "packageName": "lightbeam", 
+   "requirements": {
+    "chrome": "chrome", 
+    "sdk/tabs": "sdk/tabs", 
+    "sdk/tabs/utils": "sdk/tabs/utils", 
+    "sdk/window/utils": "sdk/window/utils"
+   }, 
+   "sectionName": "lib"
+  }, 
+  "lightbeam/ui": {
+   "docsSHA256": null, 
+   "jsSHA256": "7dabed2a1510a9b5b5e5cbb11859618aa5107cc3d93a5be4199c91d8ec4aeb5b", 
+   "moduleName": "ui", 
+   "packageName": "lightbeam", 
+   "requirements": {
+    "./connection": "lightbeam/connection", 
+    "./persist": "lightbeam/persist", 
+    "sdk/event/core": "sdk/event/core", 
+    "sdk/private-browsing": "sdk/private-browsing", 
+    "sdk/self": "sdk/self", 
+    "sdk/simple-prefs": "sdk/simple-prefs", 
+    "sdk/simple-storage": "sdk/simple-storage", 
+    "sdk/system/xul-app": "sdk/system/xul-app", 
+    "sdk/tabs": "sdk/tabs", 
+    "sdk/ui/button/action": "sdk/ui/button/action", 
+    "sdk/widget": "sdk/widget", 
+    "shared/menuitems": "lightbeam/shared/menuitems", 
+    "shared/policy": "lightbeam/shared/policy"
+   }, 
+   "sectionName": "lib"
+  }
+ }, 
+ "metadata": {
+  "addon-sdk": {
+   "description": "Add-on development made easy.", 
+   "keywords": [
+    "javascript", 
+    "engine", 
+    "addon", 
+    "extension", 
+    "xulrunner", 
+    "firefox", 
+    "browser"
+   ], 
+   "license": "MPL 2.0", 
+   "name": "addon-sdk"
+  }, 
+  "lightbeam": {
+   "author": "Mozilla Foundation", 
+   "description": "Lightbeam is a Firefox add-on that allows you to see the third parties that are collecting information about your browsing activity, with and without your consent. Using interactive visualizations, Lightbeam shows you the relationships between these third parties and the sites you visit.", 
+   "license": "MPL 2.0", 
+   "main": "main", 
+   "name": "lightbeam", 
+   "permissions": {
+    "private-browsing": true
+   }, 
+   "version": "1.0.10.2"
+  }
+ }, 
+ "name": "lightbeam", 
+ "parseable": false, 
+ "preferences": [
+  {
+   "description": "Contribute your Lightbeam data to Mozilla", 
+   "name": "contributeData", 
+   "title": "Contribute data", 
+   "type": "bool", 
+   "value": false
+  }, 
+  {
+   "description": "Graph or list", 
+   "name": "defaultVisualization", 
+   "options": [
+    {
+     "label": "Graph", 
+     "value": "graph"
+    }, 
+    {
+     "label": "List", 
+     "value": "list"
+    }
+   ], 
+   "title": "Default visualization", 
+   "type": "menulist", 
+   "value": "graph"
+  }, 
+  {
+   "description": "Time period", 
+   "name": "defaultFilter", 
+   "options": [
+    {
+     "label": "Daily", 
+     "value": "daily"
+    }, 
+    {
+     "label": "Weekly", 
+     "value": "weekly"
+    }, 
+    {
+     "label": "Recent", 
+     "value": "recent"
+    }, 
+    {
+     "label": "Last 10 sites", 
+     "value": "last10sites"
+    }
+   ], 
+   "title": "Default filter", 
+   "type": "menulist", 
+   "value": "daily"
+  }
+ ], 
+ "preferencesBranch": "jid1-F9UJ2thwoAm5gQ at jetpack", 
+ "sdkVersion": "1.16", 
+ "staticArgs": {}, 
+ "verbose": false
+}
\ No newline at end of file
diff --git a/install.rdf b/install.rdf
new file mode 100644
index 0000000..181461c
--- /dev/null
+++ b/install.rdf
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?><!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. --><RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>jid1-F9UJ2thwoAm5gQ at jetpack</em:id>
+    <em:version>1.0.10.2</em:version>
+    <em:type>2</em:type>
+    <em:bootstrap>true</em:bootstrap>
+    <em:unpack>false</em:unpack>
+
+    <!-- Firefox -->
+    <em:targetApplication>
+      <Description>
+        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+        <em:minVersion>21.0</em:minVersion>
+        <em:maxVersion>29.0a1</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+    <!-- Front End MetaData -->
+    <em:name>Lightbeam</em:name>
+    <em:description>Lightbeam is a Firefox add-on that allows you to see the third parties that are collecting information about your browsing activity, with and without your consent. Using interactive visualizations, Lightbeam shows you the relationships between these third parties and the sites you visit.</em:description>
+    <em:creator>Mozilla Foundation</em:creator>
+    
+    <em:optionsType>2</em:optionsType>
+    
+  </Description>
+</RDF>
\ No newline at end of file
diff --git a/locales.json b/locales.json
new file mode 100644
index 0000000..303e186
--- /dev/null
+++ b/locales.json
@@ -0,0 +1 @@
+{"locales": []}
diff --git a/options.xul b/options.xul
new file mode 100644
index 0000000..329cb7c
--- /dev/null
+++ b/options.xul
@@ -0,0 +1,24 @@
+<?xml version="1.0" ?>
+<vbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <setting data-jetpack-id="jid1-F9UJ2thwoAm5gQ at jetpack" pref="extensions.jid1-F9UJ2thwoAm5gQ at jetpack.contributeData" pref-name="contributeData" title="Contribute data" type="bool">Contribute your Lightbeam data to Mozilla</setting>
+  <setting data-jetpack-id="jid1-F9UJ2thwoAm5gQ at jetpack" pref="extensions.jid1-F9UJ2thwoAm5gQ at jetpack.defaultVisualization" pref-name="defaultVisualization" title="Default visualization" type="menulist">
+    Graph or list
+    <menulist>
+      <menupopup>
+        <menuitem label="Graph" value="graph"/>
+        <menuitem label="List" value="list"/>
+      </menupopup>
+    </menulist>
+  </setting>
+  <setting data-jetpack-id="jid1-F9UJ2thwoAm5gQ at jetpack" pref="extensions.jid1-F9UJ2thwoAm5gQ at jetpack.defaultFilter" pref-name="defaultFilter" title="Default filter" type="menulist">
+    Time period
+    <menulist>
+      <menupopup>
+        <menuitem label="Daily" value="daily"/>
+        <menuitem label="Weekly" value="weekly"/>
+        <menuitem label="Recent" value="recent"/>
+        <menuitem label="Last 10 sites" value="last10sites"/>
+      </menupopup>
+    </menulist>
+  </setting>
+</vbox>
diff --git a/resources/lightbeam/data/OpenSans.css b/resources/lightbeam/data/OpenSans.css
new file mode 100644
index 0000000..81cd2bc
--- /dev/null
+++ b/resources/lightbeam/data/OpenSans.css
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ at font-face {
+  font-family: 'Open Sans';
+  font-style: italic;
+  font-weight: 400;
+  src: url('font/OpenSans-LightItalic.ttf') format('truetype')
+}
+
+ at font-face {
+  font-family: 'Open Sans';
+  font-style: bold;
+  font-weight: 700;
+  src: url('font/OpenSans-Bold.ttf') format('truetype')
+}
+
+ at font-face {
+  font-family: 'Open Sans';
+  font-style: normal;
+  font-weight: 400;
+  src: url('font/OpenSans-Light.ttf') format('truetype')
+}
diff --git a/resources/lightbeam/data/aggregate.js b/resources/lightbeam/data/aggregate.js
new file mode 100644
index 0000000..c97aef4
--- /dev/null
+++ b/resources/lightbeam/data/aggregate.js
@@ -0,0 +1,523 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+// Graph Visualization
+
+// Visualization of tracking data interconnections
+
+(function (global) {
+
+"use strict";
+
+// An emitter that lists nodes and edges so we can build the data structure
+// used by all 3 visualizers.
+var aggregate = new Emitter();
+
+global.aggregate = aggregate;
+global.filteredAggregate = {
+  nodes: [],
+  edges: []
+};
+// The name of the current filter (daily, weekly, recent, last10sites)
+aggregate.currentFilter = "daily";
+
+aggregate.trackerCount = 0;
+aggregate.siteCount = 0;
+// d3 has functionality to build graphs out of lists of nodes and edges.
+aggregate.nodes = [];
+aggregate.edges = [];
+aggregate.recentSites = [];
+aggregate.initialized = false;
+aggregate.nodemap = {};
+aggregate.edgemap = {};
+
+function resetData() {
+  console.debug('aggregate::resetData');
+  aggregate.getBlockedDomains().forEach(function (domain) {
+    console.debug("deleting", domain);
+    delete userSettings[domain];
+  });
+  aggregate.nodemap = {};
+  aggregate.edgemap = {};
+  aggregate.nodes = [];
+  aggregate.edges = [];
+  aggregate.trackerCount = 0;
+  aggregate.siteCount = 0;
+  aggregate.recentSites = [];
+  global.currentVisualization.emit('reset');
+  updateStatsBar();
+}
+aggregate.on('reset', resetData);
+
+aggregate.getBlockedDomains = function () {
+  return Object.keys(userSettings).filter(function (domain) {
+    return userSettings[domain] == 'block';
+  });
+};
+
+aggregate.getAllNodes = function () {
+  var blockedDomains = aggregate.getBlockedDomains();
+  console.debug("getAllNodes", JSON.stringify(blockedDomains));
+
+  return aggregate.nodes.concat(blockedDomains.map(function (domain) {
+    return {
+      site: domain,
+      nodeType: 'blocked',
+      name: domain
+    };
+  }));
+};
+
+aggregate.getConnectionCount = function (node) {
+  if (node.nodeType === 'blocked')
+    return 0;
+
+  let connections = Object.keys(aggregate.nodeForKey(node.name)).length;
+  return connections - 1 > 0 ? connections - 1 : 0;
+};
+
+aggregate.nodeForKey = function (key) {
+  var result = {};
+  var linkedNodes = [];
+
+  if (aggregate.nodemap[key]) {
+    linkedNodes = aggregate.nodemap[key].linkedFrom.concat(aggregate.nodemap[key].linkedTo);
+    result[key] = aggregate.nodemap[key];
+  } else {
+    linkedNodes = [];
+    result[key] = {};
+  }
+
+  linkedNodes.forEach(function (nodeName) {
+    var node = aggregate.nodemap[nodeName];
+    var temp = {};
+    for (var p in node) {
+      if (node.hasOwnProperty(p) && !(p == "linkedFrom" || p == "linkedTo")) {
+        temp[p] = node[p];
+      }
+    }
+    result[nodeName] = temp;
+  });
+
+  return result;
+};
+
+aggregate.connectionAsObject = function (conn) {
+  if (Array.isArray(conn)) {
+    return {
+      source: conn[SOURCE],
+      target: conn[TARGET],
+      timestamp: new Date(conn[TIMESTAMP]),
+      contentType: conn[CONTENT_TYPE],
+      cookie: conn[COOKIE],
+      sourceVisited: conn[SOURCE_VISITED],
+      secure: conn[SECURE],
+      sourcePathDepth: conn[SOURCE_PATH_DEPTH],
+      sourceQueryDepth: conn[SOURCE_QUERY_DEPTH],
+      sourceSub: conn[SOURCE_SUB],
+      targetSub: conn[TARGET_SUB],
+      method: conn[METHOD],
+      status: conn[STATUS],
+      cacheable: conn[CACHEABLE]
+    };
+  }
+  return conn;
+
+};
+
+// Pass the list of connections to build the graph structure to pass to d3 for
+// visualizations.
+function onLoad(connections) {
+  var startTime = Date.now();
+  console.debug("aggregate::onLoad", connections.length, "connections", aggregate.currentFilter);
+  connections.forEach(onConnection);
+  aggregate.initialized = true;
+  filteredAggregate = aggregate.filters[aggregate.currentFilter]();
+
+  // Tell the visualization that we're ready.
+  if (global.currentVisualization) {
+    global.currentVisualization.emit('init');
+  }
+  updateStatsBar();
+  console.debug('aggregate::onLoad end, took %s ms', Date.now() - startTime);
+}
+
+function updateUIFromMetadata(metadata) {
+  console.debug("in aggregate metadata");
+  global.updateUIFromMetadata(metadata);
+}
+
+function updateUIFromPrefs(prefs) {
+  console.debug("in aggregate prefs");
+  global.updateUIFromPrefs(prefs);
+}
+
+aggregate.on('load', onLoad);
+aggregate.on("updateUIFromMetadata", updateUIFromMetadata);
+aggregate.on("updateUIFromPrefs", updateUIFromPrefs);
+
+// Constants for indexes of properties in array format
+const SOURCE = 0;
+const TARGET = 1;
+const TIMESTAMP = 2;
+const CONTENT_TYPE = 3;
+const COOKIE = 4;
+const SOURCE_VISITED = 5;
+const SECURE = 6;
+const SOURCE_PATH_DEPTH = 7;
+const SOURCE_QUERY_DEPTH = 8;
+const SOURCE_SUB = 9;
+const TARGET_SUB = 10;
+const METHOD = 11;
+const STATUS = 12;
+const CACHEABLE = 13;
+
+// Check that recent sites include the domain. This is another potential source
+// of false positives.
+aggregate.isDomainVisited = function isDomainVisited(domain) {
+  return aggregate.recentSites.length && (aggregate.recentSites.indexOf(domain) > -1);
+};
+
+
+function onConnection(conn) {
+  // A connection has the following keys:
+  // source (url), target (url), timestamp (int), contentType (str), cookie (bool), sourceVisited (bool), secure(bool), sourcePathDepth (int), sourceQueryDepth(int)
+  // We want to shape the collection of connections that represent points in time into
+  // aggregate data for graphing. Each connection has two endpoints represented by GraphNode objects
+  // and one edge represented by a GraphEdge object, but we want to re-use these when connections
+  // map to the same endpoints or edges.
+  var connection = aggregate.connectionAsObject(conn);
+  var sourcenode, targetnode, edge, nodelist, updated = false;
+  // Maintain the list of sites visited in dated order
+  // console.log('check for recent sites: %s: %s', connection.source, connection.sourceVisited);
+  if (connection.sourceVisited) {
+    // console.log('source visited: %s -> %s', connection.source, connection.target);
+    var site = connection.target;
+    var siteIdx = aggregate.recentSites.indexOf(site);
+    if (aggregate.recentSites.length && siteIdx === (aggregate.recentSites.length - 1)) {
+      // most recent site is already at the end of the recentSites list, do nothing
+    } else {
+
+      if (siteIdx > -1) {
+        // if site is already in list (but not last), remove it
+        aggregate.recentSites.splice(siteIdx, 1);
+      }
+      aggregate.recentSites.push(site); // push site on end of list if it is not there
+      updated = true;
+    }
+  } else {
+    // console.log('source not visited: %s -> %s', connection.source, connection.target);
+  }
+  // Retrieve the source node and update, or create it if not found
+  if (aggregate.nodemap[connection.source]) {
+    sourcenode = aggregate.nodemap[connection.source];
+    if (connection.sourceVisited && sourcenode.nodeType == "thirdparty") {
+      // the previously "thirdparty" site has now become a "visited" site
+      // +1 on visited sites counter and -1 on trackers counter
+      aggregate.siteCount++;
+      aggregate.trackerCount--;
+    }
+    sourcenode.update(connection, true);
+  } else {
+    sourcenode = new GraphNode(connection, true);
+    aggregate.nodemap[connection.source] = sourcenode;
+    aggregate.nodes.push(sourcenode);
+
+    if (connection.sourceVisited) {
+      aggregate.siteCount++;
+    } else {
+      aggregate.trackerCount++;
+    }
+    // console.log('new source: %s, now %s nodes', sourcenode.name, aggregate.nodes.length);
+    updated = true;
+  }
+  // Retrieve the target node and update, or create it if not found
+  if (aggregate.nodemap[connection.target]) {
+    targetnode = aggregate.nodemap[connection.target];
+    targetnode.update(connection, false);
+  } else {
+    targetnode = new GraphNode(connection, false);
+    aggregate.nodemap[connection.target] = targetnode;
+    aggregate.nodes.push(targetnode); // all nodes
+    if (connection.sourceVisited) {
+      aggregate.siteCount++; // Can this ever be true?
+    } else {
+      aggregate.trackerCount++;
+    }
+    // console.log('new target: %s, now %s nodes', targetnode.name, aggregate.nodes.length);
+    updated = true;
+  }
+  // Create edge objects. Could probably do this lazily just for the graph view
+  if (aggregate.edgemap[connection.source + '->' + connection.target]) {
+    edge = aggregate.edgemap[connection.source + '->' + connection.target];
+    edge.update(connection);
+  } else {
+    edge = new GraphEdge(sourcenode, targetnode, connection);
+    aggregate.edgemap[edge.name] = edge;
+    aggregate.edges.push(edge);
+    // updated = true;
+  }
+  if (updated) {
+    aggregate.update();
+  }
+  updateStatsBar();
+}
+
+aggregate.on('connection', onConnection);
+
+
+function onBlocklistUpdate({
+  domain, flag
+}) {
+  if (flag === true) {
+    userSettings[domain] = 'block';
+  } else if (userSettings[domain] == 'block') {
+    delete userSettings[domain];
+  }
+}
+aggregate.on('update-blocklist', onBlocklistUpdate);
+
+// Read the blocklist to memory
+function onBlocklistUpdateAll(domains) {
+  (domains || []).forEach(onBlocklistUpdate);
+}
+aggregate.on('update-blocklist-all', onBlocklistUpdateAll);
+
+// Used only by the graph view.
+function GraphEdge(source, target, connection) {
+  var name = source.name + '->' + target.name;
+  if (aggregate.edgemap[name]) {
+    return aggregate.edgemap[name];
+  }
+  this.source = source;
+  this.target = target;
+  this.name = name;
+  if (connection) {
+    this.cookieCount = connection.cookie ? 1 : 0;
+  }
+  return this;
+  // console.log('edge: %s', this.name);
+}
+GraphEdge.prototype.lastAccess = function () {
+  return (this.source.lastAccess > this.target.lastAccess) ? this.source.lastAccess : this.target.lastAccess;
+};
+GraphEdge.prototype.firstAccess = function () {
+  return (this.source.firstAccess < this.target.firstAccess) ? this.source.firstAccess : this.target.firstAccess;
+};
+GraphEdge.prototype.update = function (connection) {
+  this.cookieCount = connection.cookie ? this.cookieCount + 1 : this.cookieCount;
+};
+
+// A graph node represents one end of a connection, either a target or a source
+// Where a connection is a point in time with a timestamp, a graph node has a  time range
+// represented by firstAccess and lastAccess. Where a connection has a contentType, a node
+// has an array of content types. Booleans in graph nodes become boolean pairs in graph nodes
+// (for instance, some connections may have cookies and some may not, which would result in both
+// cookie and notCookie being true). We set an initial position randomly to keep the force graph
+// from exploding.
+//
+function GraphNode(connection, isSource) {
+  this.firstAccess = this.lastAccess = connection.timestamp;
+  this.linkedFrom = [];
+  this.linkedTo = [];
+  this.contentTypes = [];
+  this.subdomain = [];
+  this.method = [];
+  this.status = [];
+  this.visitedCount = 0;
+  this.secureCount = 0;
+  this.cookieCount = 0;
+  this.howMany = 0;
+  if (connection) {
+    this.update(connection, isSource);
+  }
+  // FIXME: Get the width and height from the add-on somehow
+  var width = 1000;
+  var height = 1000;
+  // Set defaults for graph
+  this.x = this.px = (Math.random() - 0.5) * 800 + width / 2;
+  this.y = this.py = (Math.random() - 0.5) * 800 + height / 2;
+  this.weight = 0;
+}
+GraphNode.prototype.update = function (connection, isSource) {
+  if (!this.name) {
+    this.name = isSource ? connection.source : connection.target;
+    // console.log('node: %s', this.name);
+  }
+  if (connection.timestamp > this.lastAccess) {
+    this.lastAccess = connection.timestamp;
+  }
+  if (connection.timestamp < this.firstAccess) {
+    this.firstAccess = connection.timestamp;
+  }
+  if (isSource && (this.linkedTo.indexOf(connection.target) < 0)) {
+    this.linkedTo.push(connection.target);
+  }
+  if ((!isSource) && (this.linkedFrom.indexOf(connection.source) < 0)) {
+    this.linkedFrom.push(connection.source);
+  }
+  if (this.contentTypes.indexOf(connection.contentType) < 0) {
+    this.contentTypes.push(connection.contentType);
+  }
+  if (connection.sourceVisited) {
+    this.visitedCount++;
+  }
+  if (this.subdomain.indexOf(connection.sourceSub) < 0) {
+    this.subdomain.push(connection.sourceSub);
+  }
+  if (connection.cookie) {
+    this.cookieCount++;
+  }
+  if (connection.secure) {
+    this.secureCount++;
+  }
+  if (this.method.indexOf(connection.method) < 0) {
+    this.method.push(connection.method);
+  }
+  if (this.status.indexOf(connection.status) < 0) {
+    this.status.push(connection.status);
+  }
+
+  this.howMany++;
+  if (this.visitedCount / this.howMany == 1) {
+    this.nodeType = 'site';
+  } else if (this.visitedCount / this.howMany === 0) {
+    this.nodeType = 'thirdparty';
+  } else {
+    this.nodeType = 'both';
+  }
+  return this;
+};
+
+// Filtering
+
+function sitesSortedByDate() {
+  return aggregate.recentSites.map(function (sitename) {
+    return aggregate.nodemap[sitename];
+  });
+}
+aggregate.sitesSortedByDate = sitesSortedByDate;
+
+function aggregateFromNodes(nodes) {
+  var localmap = {};
+  var edgemap = {};
+  nodes.forEach(function (node) {
+    localmap[node.name] = node;
+    node.linkedFrom.forEach(function (nodename) {
+      var linkedNode = aggregate.nodemap[nodename];
+      var edge = new GraphEdge(node, linkedNode);
+      edgemap[edge.name] = edge;
+      localmap[nodename] = linkedNode;
+    });
+    node.linkedTo.forEach(function (nodename) {
+      var linkedNode = aggregate.nodemap[nodename];
+      var edge = new GraphEdge(node, linkedNode);
+      edgemap[edge.name] = edge;
+      localmap[nodename] = linkedNode;
+    });
+  });
+  return {
+    nodes: Object.keys(localmap).map(function (name) {
+      return localmap[name];
+    }),
+    edges: Object.keys(edgemap).map(function (name) {
+      return edgemap[name];
+    })
+  };
+}
+
+// filters
+aggregate.filters = {
+  daily: function daily() {
+    var now = Date.now();
+    var then = now - (24 * 60 * 60 * 1000);
+    var sortedNodes = sitesSortedByDate();
+    // find index where we go beyond date
+    var i;
+    for (i = sortedNodes.length - 1; i > -1; i--) {
+      if (sortedNodes[i].lastAccess.valueOf() < then) {
+        break;
+      }
+    }
+    // i is always 1 too low at the point
+    i++; // put it back
+    var filteredNodes = sortedNodes.slice(i);
+    // Done filtering
+    return aggregateFromNodes(filteredNodes);
+  },
+  weekly: function weekly() {
+    var now = Date.now();
+    var then = now - (7 * 24 * 60 * 60 * 1000);
+    var sortedNodes = sitesSortedByDate();
+    // find index where we go beyond date
+    var i;
+    for (i = sortedNodes.length - 1; i > -1; i--) {
+      if (sortedNodes[i].lastAccess < then) {
+        break;
+      }
+    }
+    i++; // we decrement too far, put it back
+    var filteredNodes = sortedNodes.slice(i);
+    // console.log('weekly filter after: %s', filteredNodes.length);
+    return aggregateFromNodes(filteredNodes);
+  },
+  last10sites: function last10sites() {
+    var sortedNodes = sitesSortedByDate();
+    var filteredNodes = sortedNodes.slice(-10);
+    return aggregateFromNodes(filteredNodes);
+  },
+  recent: function recent() {
+    var sortedNodes = sitesSortedByDate();
+    var filteredNodes = sortedNodes.slice(-1);
+    return aggregateFromNodes(filteredNodes);
+  }
+};
+
+function switchFilter(name) {
+  if (aggregate.currentFilter == name) {
+    return;
+  }
+  aggregate.currentFilter = name;
+  global.self.port.emit("prefChanged", {
+    defaultFilter: name
+  });
+  aggregate.update();
+}
+
+aggregate.switchFilter = switchFilter;
+
+// Underscore debounce function
+//
+// Returns a function, that, as long as it continues to be invoked, will not
+// be triggered. The function will be called after it stops being called for
+// N milliseconds. If `immediate` is passed, trigger the function on the
+// leading edge, instead of the trailing.
+var debounce = function debounce(func, wait, immediate) {
+  var timeout;
+  return function () {
+    var context = this,
+      args = arguments;
+    var later = function () {
+      timeout = null;
+      if (!immediate) func.apply(context, args);
+    };
+    var callNow = immediate && !timeout;
+    clearTimeout(timeout);
+    timeout = setTimeout(later, wait);
+    if (callNow) func.apply(context, args);
+  };
+};
+
+aggregate.update = debounce(function update() {
+  // FIXME: maybe not for list view
+  if (global.currentVisualization && global.currentVisualization.name !== 'graph') {
+    console.log('do not update aggregate for view', currentVisualization.name);
+  }
+  if (aggregate.initialized) {
+    global.filteredAggregate = aggregate.filters[aggregate.currentFilter]();
+    aggregate.emit('update');
+  }
+  updateStatsBar();
+});
+
+})(this);
diff --git a/resources/lightbeam/data/content-script.js b/resources/lightbeam/data/content-script.js
new file mode 100644
index 0000000..5de2a1d
--- /dev/null
+++ b/resources/lightbeam/data/content-script.js
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+(function (global) {
+
+// This is the e10s/message passing content script that ties the workers to the
+// addon. It can see most of the addon, the window is either not visible or not
+// mutable so we use unsafeWindow below. This handles the post message
+// connections and does a little UI work on the side.
+self.port.on('connection', function (connection) {
+  global.allConnections.push(connection);
+  global.aggregate.emit('connection', connection);
+});
+
+self.port.on('passStoredConnections', function (connections) {
+  global.allConnections = connections;
+  global.aggregate.emit('load', global.allConnections);
+});
+
+self.port.on('update-blocklist', function (domain) {
+  global.aggregate.emit('update-blocklist', domain);
+});
+
+self.port.on('update-blocklist-all', function (domains) {
+  global.aggregate.emit('update-blocklist-all', domains);
+});
+
+self.port.on('init', function () {
+  console.debug('content-script::init()');
+  global.aggregate.emit('load', global.allConnections);
+});
+
+self.port.on("updateUIFromMetadata", function (metadata) {
+  console.debug("Got add-on metadata", metadata);
+  global.aggregate.emit("updateUIFromMetadata", metadata);
+});
+
+self.port.on("updateUIFromPrefs", function (prefs) {
+  console.debug("Got set prefs", prefs);
+  global.aggregate.emit("updateUIFromPrefs", prefs);
+});
+
+})(this);
diff --git a/resources/lightbeam/data/dialog.js b/resources/lightbeam/data/dialog.js
new file mode 100644
index 0000000..072d1cb
--- /dev/null
+++ b/resources/lightbeam/data/dialog.js
@@ -0,0 +1,257 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* Dialog / Popup ===================================== */
+
+// dialog names (used as dialog identifiers)
+const dialogNames = {
+  "promptToShare": "promptToShareData",
+  "resetData": "resetData",
+  "blockSites": "blockSites",
+  "hideSites": "hideSites",
+  "startUploadData": "startUploadData",
+  "stopUploadData": "stopUploadData",
+  "privateBrowsing": "privateBrowsing",
+  "saveOldData": "saveOldData"
+};
+
+const allDialogs = {
+  'Reset Data Confirmation': confirmResetDataDialog,
+  'Block Sites Confirmation': confirmBlockSitesDialog,
+  'Hide Sites Confirmation': confirmHideSitesDialog,
+  'Upload Data Confirmation': askForDataSharingConfirmationDialog,
+  'Stop Uploading Data Confirmation': stopSharingDialog,
+  'Private Browsing Notification': informUserOfUnsafeWindowsDialog,
+};
+
+// options: name, title, message, type, dnsPrompt(Do Not Show), imageUrl
+function dialog(options, callback) {
+  createDialog(options, callback);
+}
+
+function createDialog(options, callback) {
+  var modal = picoModal({
+    content: createDialogContent(options),
+    closeButton: false,
+    overlayClose: false,
+    overlayStyles: {
+      backgroundColor: "#000",
+      opacity: 0.75
+    }
+  });
+
+  addDialogEventHandlers(modal, options, function (userResponse) {
+    callback(userResponse);
+  });
+}
+
+function createDialogContent(options) {
+  return dialogTitleBar(options) +
+    dialogMessage(options) +
+    dialogControls(options);
+}
+
+function dialogTitleBar(options) {
+  return "<div class='dialog-title'>" + (options.title || " ") + "</div>";
+}
+
+function dialogMessage(options) {
+  return "<div class='dialog-content'>" +
+    (options.imageUrl ? "<div class='dialog-sign'><img src='" + options.imageUrl + "' /></div>" : "") +
+    "<div class='dialog-message'>" + (options.message || " ") + "</div>" +
+    "</div>";
+}
+
+function dialogControls(options) {
+  var doNotShowAgainPrompt = "<div class='dialog-dns'><input type='checkbox' /> Do not show this again.</div>";
+  // control buttons
+  var controlButtons = "<div class='dialog-btns'>";
+  var okButton = "<a class='pico-close dialog-ok'>OK</a>";
+  var cancelButton = "<a class='pico-close dialog-cancel'>Cancel</a>";
+  // check dialog type
+  // alert dialog only needs a single button - "OK"
+  // else we show both "OK" and "Cancel" buttons
+  if (options.type == "alert") {
+    controlButtons += "<a class='pico-close dialog-ok'>OK</a>";
+
+  } else {
+    if (navigator.appVersion.indexOf("Win") > -1) { // runs on Windows
+      controlButtons += okButton + cancelButton;
+    } else { // runs on OS other than Windows
+      controlButtons += cancelButton + okButton;
+    }
+  }
+  controlButtons += "</div>";
+
+  return "<div class='dialog-controls'>" +
+    (options.dnsPrompt ? doNotShowAgainPrompt : "") +
+    controlButtons +
+    "</div>";
+}
+
+function addDialogEventHandlers(modal, options, callback) {
+  var dialogContainer = modal.modalElem;
+  // OK button click event handler
+  var okButton = dialogContainer.querySelector(".pico-close.dialog-ok");
+  okButton.addEventListener("click", function () {
+    if (dialogContainer.querySelector(".dialog-dns input") && dialogContainer.querySelector(".dialog-dns input").checked) { // Do Not Show
+    }
+    modal.close();
+    callback(true);
+  });
+  // Cancel button click event handler
+  var cancelButton = dialogContainer.querySelector(".pico-close.dialog-cancel");
+  if (cancelButton) {
+    cancelButton.addEventListener("click", function () {
+      if (options.name == dialogNames.promptToShare) {
+        if (dialogContainer.querySelector(".dialog-dns input").checked) {}
+      }
+      modal.close();
+      callback(false);
+    });
+  }
+
+  var keyDownHandler = function (event) {
+    // disable Tab
+    if (event.keyCode == "9") {
+      event.preventDefault();
+    }
+    // press Esc to close the dialog (functions the same as clicking Cancel)
+    if (event.keyCode == "27") { // Esc key pressed
+      modal.close();
+      callback(false);
+    }
+  };
+  document.addEventListener("keydown", keyDownHandler);
+
+  modal.onClose(function () {
+    document.removeEventListener("keydown", keyDownHandler);
+  });
+
+  // for Upload Data dialog
+  if (dialogContainer.querySelector(".toggle-pp")) {
+    dialogContainer.querySelector(".toggle-pp").addEventListener("click", function (event) {
+      dialogContainer.querySelector(".pico-content .privacy-policy").classList.toggle("collapsed");
+    });
+  }
+
+  restrictTabWithinDialog(modal);
+}
+
+function restrictTabWithinDialog(modal) {
+  var dialogContainer = modal.modalElem;
+  assignTabIndices(modal);
+  dialogContainer.addEventListener("keypress", function (event) {
+    event.stopPropagation();
+    var focusedElm = document.activeElement;
+    // Tab key is pressed
+    if (event.keyCode == "9") {
+      var currentTabIndex = parseInt(focusedElm.getAttribute("tabIndex"));
+      var nextElem = dialogContainer.querySelector("[tabIndex='" + (currentTabIndex + 1) + "']");
+      if (nextElem) {
+        nextElem.focus();
+      } else {
+        dialogContainer.querySelector("[tabIndex='0']").focus();
+      }
+    }
+    // when the focused element is the OK or Cancel button and Enter key is pressed
+    // mimic mouse clicking on button
+    if (event.keyCode == "13" && focusedElm.mozMatchesSelector(".pico-content .dialog-btns a")) {
+      focusedElm.click();
+    }
+  });
+}
+
+function assignTabIndices(modal) {
+  var dialogContainer = modal.modalElem;
+  var allElemsInDialog = dialogContainer.querySelectorAll("*");
+  var tabIndex = 0;
+  toArray(allElemsInDialog).forEach(function (elem, i) {
+    if (elem.nodeName.toLowerCase() == "a" || elem.nodeName.toLowerCase() == "input") {
+      elem.setAttribute("tabIndex", tabIndex);
+      tabIndex++;
+    }
+  });
+  dialogContainer.querySelector("[tabIndex='0']").focus();
+}
+
+function askForDataSharingConfirmationDialog(callback) {
+  dialog({
+      "name": dialogNames.startUploadData,
+      "title": "Upload Data",
+      "message": '<p>You are about to start uploading data to the Lightbeam server. Your data will continue to be uploaded periodically until you turn off sharing. For more information about the data we upload, how we take steps to minimize risk of re-identification, and what Mozilla\'s privacy policies are, please read the  <a class="toggle-pp">the Lightbeam Privacy Policy</a>.</p>' +
+      // Lightbeam Privacy Policy.
+      '<div class="privacy-policy"> <header><b>Lightbeam Privacy Notice</b></header> <p> We care about your privacy. Lightbeam is a browser add-on that collects and helps you visualize third party requests on any site you visit. If you choose to send Lightbeam data to Mozilla (that’s us), our <a href="#mozillaprivacypolicy">privacy policy</a> describes how we handle that data. </p> <header><b>Things you should know</b></header> <ul class="bullet-form"> <li> After you install Lightbeam, t [...]
+      // Lightbeam Privacy Policy ends
+      '<br />' +
+        '<p>By clicking OK, you are agreeing to the data practices in our privacy notice.</p>',
+      "imageUrl": "image/lightbeam_popup_warningsharing.png"
+    },
+    callback);
+}
+
+function stopSharingDialog(callback) {
+  dialog({
+      "name": dialogNames.stopUploadData,
+      "title": "Stop Uploading Data",
+      "message": '<p>You are about to stop sharing data with the Lightbeam server.</p>' +
+        '<p>By clicking OK you will no longer be uploading data.</p>',
+      "imageUrl": "image/lightbeam_popup_stopsharing2.png"
+    },
+    function (confirmed) {
+      callback(confirmed);
+    }
+  );
+}
+
+function informUserOfUnsafeWindowsDialog() {
+  dialog({
+      "type": "alert",
+      "name": dialogNames.privateBrowsing,
+      "dnsPrompt": true,
+      "title": "Private Browsing",
+      "message": "<p>You have one or more private browsing windows open.</p>" +
+        "<p>Connections made in private browsing windows will be visualized in Lightbeam but that data is neither stored locally nor will it ever be shared, even if sharing is enabled. </p>" +
+        "<p> Information gathered in private browsing mode will be deleted whenever Lightbeam is restarted, and is not collected at all when Lightbeam is not open..</p>",
+      "imageUrl": "image/lightbeam_popup_privacy.png"
+    },
+    function (confirmed) {}
+  );
+}
+
+
+function confirmBlockSitesDialog(callback) {
+  dialog({
+      "name": dialogNames.blockSites,
+      "title": "Block Sites",
+      "message": "<p><b>Warning:</b></p> " +
+        "<p>Blocking sites will prevent any and all content from being loaded from selected domains, for example: [example.com, example.net] and all of their subdomains [mail.example.com, news.example.net etc.]. </p>" +
+        "<p>This can prevent some sites from working and degrade your internet experience. Please use this feature carefully. </p>",
+      "imageUrl": "image/lightbeam_popup_blocked.png"
+    },
+    callback
+  );
+}
+
+function confirmHideSitesDialog(callback) {
+  dialog({
+      "name": dialogNames.hideSites,
+      "dnsPrompt": true,
+      "title": "Hide Sites",
+      "message": "<p>These sites will not be shown in Lightbeam visualizations, including List View, unless you specifically toggle them back on with the Show Hidden Sites button.</p>" +
+        "<p>You can use this to ignore trusted sites from the data.</p>",
+      "imageUrl": "image/lightbeam_popup_hidden.png"
+    },
+    callback
+  );
+}
+
+function confirmResetDataDialog(callback) {
+  dialog({
+    "name": dialogNames.resetData,
+    "title": "Reset Data",
+    "message": "<p>Pressing OK will delete all Lightbeam information including connection history, user preferences, block sites list etc.</p>" +
+      "<p>Your browser will be returned to the state of a fresh install of Lightbeam.</p>",
+    "imageUrl": "image/lightbeam_popup_warningreset.png"
+  }, callback);
+}
diff --git a/resources/lightbeam/data/events.js b/resources/lightbeam/data/events.js
new file mode 100644
index 0000000..3d9cfd3
--- /dev/null
+++ b/resources/lightbeam/data/events.js
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+// Basic implementation of an event emitter for visualization plugins
+// This may be built-in to jetpack, but it's not available on the HTML side so
+// we need to keep this.
+function Emitter() {
+  this._listeners = {};
+}
+
+Emitter.prototype.on = function on(eventName, listener) {
+  if (!this._listeners[eventName]) {
+    this._listeners[eventName] = [];
+  }
+  this._listeners[eventName].push(listener);
+};
+
+Emitter.prototype.once = function once(eventName, listener) {
+  var self = this;
+  var wrapped = function wrapped(msg1, msg2, msg3) {
+    listener(msg1, msg2, msg3);
+    self.removeListener(eventName, wrapped);
+  };
+  this.on(eventName, wrapped);
+};
+
+Emitter.prototype.off = function off(eventName, listener) {
+  if (!this._listeners[eventName]) return;
+  var listenerIndex = this._listeners[eventName].indexOf(listener);
+  if (listenerIndex < 0) return;
+  this._listeners[eventName].splice(listenerIndex, 1);
+};
+
+Emitter.prototype.removeAllListeners = function removeAllListeners(eventName) {
+  this._listeners[eventName] = [];
+};
+
+Emitter.prototype.clear = function clear() {
+  this._listeners = {};
+};
+
+Emitter.prototype.emit = function emit(eventName, message, msg2, msg3) {
+  if (!this._listeners[eventName]) return;
+  this._listeners[eventName].forEach(function (listener) {
+    listener(message, msg2, msg3);
+  });
+};
diff --git a/resources/lightbeam/data/first-run.css b/resources/lightbeam/data/first-run.css
new file mode 100644
index 0000000..57eaa02
--- /dev/null
+++ b/resources/lightbeam/data/first-run.css
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+body {
+  background: #5d667a;
+  color:#333;
+  font-family: "Open Sans", sans-serif;
+  margin:0; /* unset browser styles */
+  padding-bottom:16px;
+}
+
+.center { text-align: center; }
+img.center, button.center { display: block; margin: 0 auto; }
+
+img#lightbeam-logo { margin-bottom:-18px; }
+
+body > ol, body > ul { width: 578px; margin: 0 auto; }
+
+ol#steps {
+  /* unset default styles */
+  list-style-type:none;
+  padding:0px;
+  /* counter for custom numbers */
+  counter-reset: steps-counter;
+}
+ol#steps > li {
+  background:white;
+  padding:20px;
+  border-radius: 0.5em;
+  margin-bottom:24px;
+  position:relative;
+}
+ol#steps > li:before {
+  content: counter(steps-counter);
+  counter-increment: steps-counter;
+  font-size:31px;
+  font-weight:bold;
+  position: absolute;
+  top: 8px;
+  left: -68px;
+  background:white;
+  width:48px;
+  height:48px;
+  border-radius:24px;
+  text-align:center;
+}
+ol#steps p {
+  margin:0; /* unset default styles */
+  margin-bottom: 16px;
+}
+ol#steps > li p:last-child {
+  margin-bottom: 0;
+}
+
+ul#contact {
+  padding-left:16px
+}
+
+button#openLightbeam {
+  font-size:18px;
+  font-weight:strong;
+  padding:8px;
+}
diff --git a/resources/lightbeam/data/first-run.html b/resources/lightbeam/data/first-run.html
new file mode 100644
index 0000000..6dc76b9
--- /dev/null
+++ b/resources/lightbeam/data/first-run.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Welcome to Lightbeam!</title>
+    <link rel="stylesheet" href="OpenSans.css" />
+    <link rel="stylesheet" href="first-run.css" />
+    <script>
+      document.addEventListener("DOMContentLoaded", function() {
+        const firefoxVersionRe = /Firefox\/(([0-9]+)\.([0-9]+))/;
+        const firefoxMajorVersion = Number(firefoxVersionRe.exec(window.navigator.userAgent)[2]);
+        const usingAustralis = firefoxMajorVersion >= 29 ? true : false;
+
+        const australisSubs = {
+          ".platform-dependent, #icon-placement": '<img src="icons/lightbeam_logo-only_32x32.png" style="width:20px;"/> logo in the Toolbar.'
+        }
+
+        if (usingAustralis) {
+          for (var qs in australisSubs) {
+            document.querySelector(qs).innerHTML = australisSubs[qs];
+          }
+        }
+      });
+    </script>
+  </head>
+  <body>
+
+    <div id="header">
+      <img src="icons/lightbeam_logo-wordmark_300x150.png" class="center" id="lightbeam-logo" />
+      <h1 class="center">Thanks for downloading Lightbeam!</h1>
+    </div>
+
+    <ol id="steps">
+      <li>
+        <strong>Open the Lightbeam tab</strong> by clicking the <span class="platform-dependent" id="icon-placement"><img src="icons/lightbeam_logo-only_16x16.png" /> logo in the Add-on bar.</span>
+      </li>
+      <li>
+        <p>Lightbeam starts recording connections as soon as it's installed. If you open it now, you'll see a blank screen because nothing's been recorded yet.</p>
+        <p><strong>To start visualizing your online interactions</strong>, open a new tab, navigate to a site, and then check back to the Lightbeam tab to see how your connections graph appears.</p>
+      </li>
+      <li>
+        <p><strong>Consider contributing data to Mozilla!</strong> We'll use it to study the tracking landscape and develop new privacy-protecting technologies for the web.</p>
+        <p>You can opt in to sharing your data by flipping the "Contribute Data" toggle in the top right corner of the Lightbeam tab to "On".</p>
+      </li>
+      <li>
+        <ul id="contact">
+          <li>Learn more about Mozilla's Lightbeam for Firefox on our <a href="https://mozilla.org/lightbeam">project page</a></li>
+          <li>Question? Idea? Email the mailing list: <a href="mailto:lightbeam-feedback at mozilla.org" target="_blank">lightbeam at mozilla.org</a></li>
+          <li>Found a bug? <a href="https://github.com/mozilla/lightbeam/issues/new" target="_blank">Report an issue</a> on Github</li>
+          <li>Don't forget to <a href="https://addons.mozilla.org/en-US/firefox/addon/lightbeam/reviews/add" target="_blank">leave a review</a> on our Mozilla Addons page!</li>
+        </ul>
+      </li>
+    </ol>
+
+  </body>
+</html>
diff --git a/resources/lightbeam/data/font-awesome.css b/resources/lightbeam/data/font-awesome.css
new file mode 100644
index 0000000..832d1ad
--- /dev/null
+++ b/resources/lightbeam/data/font-awesome.css
@@ -0,0 +1,537 @@
+/*!
+ *  Font Awesome 3.0.1
+ *  the iconic font designed for use with Twitter Bootstrap
+ *  -------------------------------------------------------
+ *  The full suite of pictographic icons, examples, and documentation
+ *  can be found at: http://fortawesome.github.com/Font-Awesome/
+ *
+ *  License
+ *  -------------------------------------------------------
+ *  - The Font Awesome font is licensed under the SIL Open Font License - http://scripts.sil.org/OFL
+ *  - Font Awesome CSS, LESS, and SASS files are licensed under the MIT License -
+ *    http://opensource.org/licenses/mit-license.html
+ *  - The Font Awesome pictograms are licensed under the CC BY 3.0 License - http://creativecommons.org/licenses/by/3.0/
+ *  - Attribution is no longer required in Font Awesome 3.0, but much appreciated:
+ *    "Font Awesome by Dave Gandy - http://fortawesome.github.com/Font-Awesome"
+
+ *  Contact
+ *  -------------------------------------------------------
+ *  Email: dave at davegandy.com
+ *  Twitter: http://twitter.com/fortaweso_me
+ *  Work: Lead Product Designer @ http://kyruus.com
+ */
+ at font-face {
+  font-family: 'FontAwesome';
+  src: url('font/fontawesome-webfont.woff?') format('woff');
+  font-weight: normal;
+  font-style: normal;
+}
+/*  Font Awesome styles
+    ------------------------------------------------------- */
+[class^="icon-"],
+[class*=" icon-"] {
+  font-family: FontAwesome;
+  font-weight: normal;
+  font-style: normal;
+  text-decoration: inherit;
+  -webkit-font-smoothing: antialiased;
+
+  /* sprites.less reset */
+  display: inline;
+  width: auto;
+  height: auto;
+  line-height: normal;
+  vertical-align: baseline;
+  background-image: none;
+  background-position: 0% 0%;
+  background-repeat: repeat;
+  margin-top: 0;
+}
+/* more sprites.less reset */
+.icon-white,
+.nav-pills > .active > a > [class^="icon-"],
+.nav-pills > .active > a > [class*=" icon-"],
+.nav-list > .active > a > [class^="icon-"],
+.nav-list > .active > a > [class*=" icon-"],
+.navbar-inverse .nav > .active > a > [class^="icon-"],
+.navbar-inverse .nav > .active > a > [class*=" icon-"],
+.dropdown-menu > li > a:hover > [class^="icon-"],
+.dropdown-menu > li > a:hover > [class*=" icon-"],
+.dropdown-menu > .active > a > [class^="icon-"],
+.dropdown-menu > .active > a > [class*=" icon-"],
+.dropdown-submenu:hover > a > [class^="icon-"],
+.dropdown-submenu:hover > a > [class*=" icon-"] {
+  background-image: none;
+}
+[class^="icon-"]:before,
+[class*=" icon-"]:before {
+  text-decoration: inherit;
+  display: inline-block;
+  speak: none;
+}
+/* makes sure icons active on rollover in links */
+a [class^="icon-"],
+a [class*=" icon-"] {
+  display: inline-block;
+}
+/* makes the font 33% larger relative to the icon container */
+.icon-large:before {
+  vertical-align: -10%;
+  font-size: 1.3333333333333333em;
+}
+.btn [class^="icon-"],
+.nav [class^="icon-"],
+.btn [class*=" icon-"],
+.nav [class*=" icon-"] {
+  display: inline;
+  /* keeps button heights with and without icons the same */
+
+}
+.btn [class^="icon-"].icon-large,
+.nav [class^="icon-"].icon-large,
+.btn [class*=" icon-"].icon-large,
+.nav [class*=" icon-"].icon-large {
+  line-height: .9em;
+}
+.btn [class^="icon-"].icon-spin,
+.nav [class^="icon-"].icon-spin,
+.btn [class*=" icon-"].icon-spin,
+.nav [class*=" icon-"].icon-spin {
+  display: inline-block;
+}
+.nav-tabs [class^="icon-"],
+.nav-pills [class^="icon-"],
+.nav-tabs [class*=" icon-"],
+.nav-pills [class*=" icon-"] {
+  /* keeps button heights with and without icons the same */
+
+}
+.nav-tabs [class^="icon-"],
+.nav-pills [class^="icon-"],
+.nav-tabs [class*=" icon-"],
+.nav-pills [class*=" icon-"],
+.nav-tabs [class^="icon-"].icon-large,
+.nav-pills [class^="icon-"].icon-large,
+.nav-tabs [class*=" icon-"].icon-large,
+.nav-pills [class*=" icon-"].icon-large {
+  line-height: .9em;
+}
+li [class^="icon-"],
+.nav li [class^="icon-"],
+li [class*=" icon-"],
+.nav li [class*=" icon-"] {
+  display: inline-block;
+  width: 1.25em;
+  text-align: center;
+}
+li [class^="icon-"].icon-large,
+.nav li [class^="icon-"].icon-large,
+li [class*=" icon-"].icon-large,
+.nav li [class*=" icon-"].icon-large {
+  /* increased font size for icon-large */
+
+  width: 1.5625em;
+}
+ul.icons {
+  list-style-type: none;
+  text-indent: -0.75em;
+}
+ul.icons li [class^="icon-"],
+ul.icons li [class*=" icon-"] {
+  width: .75em;
+}
+.icon-muted {
+  color: #eeeeee;
+}
+.icon-border {
+  border: solid 1px #eeeeee;
+  padding: .2em .25em .15em;
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+}
+.icon-2x {
+  font-size: 2em;
+}
+.icon-2x.icon-border {
+  border-width: 2px;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+}
+.icon-3x {
+  font-size: 3em;
+}
+.icon-3x.icon-border {
+  border-width: 3px;
+  -webkit-border-radius: 5px;
+  -moz-border-radius: 5px;
+  border-radius: 5px;
+}
+.icon-4x {
+  font-size: 4em;
+}
+.icon-4x.icon-border {
+  border-width: 4px;
+  -webkit-border-radius: 6px;
+  -moz-border-radius: 6px;
+  border-radius: 6px;
+}
+.pull-right {
+  float: right;
+}
+.pull-left {
+  float: left;
+}
+[class^="icon-"].pull-left,
+[class*=" icon-"].pull-left {
+  margin-right: .3em;
+}
+[class^="icon-"].pull-right,
+[class*=" icon-"].pull-right {
+  margin-left: .3em;
+}
+.btn [class^="icon-"].pull-left.icon-2x,
+.btn [class*=" icon-"].pull-left.icon-2x,
+.btn [class^="icon-"].pull-right.icon-2x,
+.btn [class*=" icon-"].pull-right.icon-2x {
+  margin-top: .18em;
+}
+.btn [class^="icon-"].icon-spin.icon-large,
+.btn [class*=" icon-"].icon-spin.icon-large {
+  line-height: .8em;
+}
+.btn.btn-small [class^="icon-"].pull-left.icon-2x,
+.btn.btn-small [class*=" icon-"].pull-left.icon-2x,
+.btn.btn-small [class^="icon-"].pull-right.icon-2x,
+.btn.btn-small [class*=" icon-"].pull-right.icon-2x {
+  margin-top: .25em;
+}
+.btn.btn-large [class^="icon-"],
+.btn.btn-large [class*=" icon-"] {
+  margin-top: 0;
+}
+.btn.btn-large [class^="icon-"].pull-left.icon-2x,
+.btn.btn-large [class*=" icon-"].pull-left.icon-2x,
+.btn.btn-large [class^="icon-"].pull-right.icon-2x,
+.btn.btn-large [class*=" icon-"].pull-right.icon-2x {
+  margin-top: .05em;
+}
+.btn.btn-large [class^="icon-"].pull-left.icon-2x,
+.btn.btn-large [class*=" icon-"].pull-left.icon-2x {
+  margin-right: .2em;
+}
+.btn.btn-large [class^="icon-"].pull-right.icon-2x,
+.btn.btn-large [class*=" icon-"].pull-right.icon-2x {
+  margin-left: .2em;
+}
+.icon-spin {
+  display: inline-block;
+  -moz-animation: spin 2s infinite linear;
+  -o-animation: spin 2s infinite linear;
+  -webkit-animation: spin 2s infinite linear;
+  animation: spin 2s infinite linear;
+}
+ at -moz-keyframes spin {
+  0% { -moz-transform: rotate(0deg); }
+  100% { -moz-transform: rotate(359deg); }
+}
+ at -webkit-keyframes spin {
+  0% { -webkit-transform: rotate(0deg); }
+  100% { -webkit-transform: rotate(359deg); }
+}
+ at -o-keyframes spin {
+  0% { -o-transform: rotate(0deg); }
+  100% { -o-transform: rotate(359deg); }
+}
+ at -ms-keyframes spin {
+  0% { -ms-transform: rotate(0deg); }
+  100% { -ms-transform: rotate(359deg); }
+}
+ at keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(359deg); }
+}
+ at -moz-document url-prefix() {
+  .icon-spin {
+    height: .9em;
+  }
+  .btn .icon-spin {
+    height: auto;
+  }
+  .icon-spin.icon-large {
+    height: 1.25em;
+  }
+  .btn .icon-spin.icon-large {
+    height: .75em;
+  }
+}
+/*  Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
+    readers do not read off random characters that represent icons */
+.icon-glass:before                { content: "\f000"; }
+.icon-music:before                { content: "\f001"; }
+.icon-search:before               { content: "\f002"; }
+.icon-envelope:before             { content: "\f003"; }
+.icon-heart:before                { content: "\f004"; }
+.icon-star:before                 { content: "\f005"; }
+.icon-star-empty:before           { content: "\f006"; }
+.icon-user:before                 { content: "\f007"; }
+.icon-film:before                 { content: "\f008"; }
+.icon-th-large:before             { content: "\f009"; }
+.icon-th:before                   { content: "\f00a"; }
+.icon-th-list:before              { content: "\f00b"; }
+.icon-ok:before                   { content: "\f00c"; }
+.icon-remove:before               { content: "\f00d"; }
+.icon-zoom-in:before              { content: "\f00e"; }
+
+.icon-zoom-out:before             { content: "\f010"; }
+.icon-off:before                  { content: "\f011"; }
+.icon-signal:before               { content: "\f012"; }
+.icon-cog:before                  { content: "\f013"; }
+.icon-trash:before                { content: "\f014"; }
+.icon-home:before                 { content: "\f015"; }
+.icon-file:before                 { content: "\f016"; }
+.icon-time:before                 { content: "\f017"; }
+.icon-road:before                 { content: "\f018"; }
+.icon-download-alt:before         { content: "\f019"; }
+.icon-download:before             { content: "\f01a"; }
+.icon-upload:before               { content: "\f01b"; }
+.icon-inbox:before                { content: "\f01c"; }
+.icon-play-circle:before          { content: "\f01d"; }
+.icon-repeat:before               { content: "\f01e"; }
+
+/* \f020 doesn't work in Safari. all shifted one down */
+.icon-refresh:before              { content: "\f021"; }
+.icon-list-alt:before             { content: "\f022"; }
+.icon-lock:before                 { content: "\f023"; }
+.icon-flag:before                 { content: "\f024"; }
+.icon-headphones:before           { content: "\f025"; }
+.icon-volume-off:before           { content: "\f026"; }
+.icon-volume-down:before          { content: "\f027"; }
+.icon-volume-up:before            { content: "\f028"; }
+.icon-qrcode:before               { content: "\f029"; }
+.icon-barcode:before              { content: "\f02a"; }
+.icon-tag:before                  { content: "\f02b"; }
+.icon-tags:before                 { content: "\f02c"; }
+.icon-book:before                 { content: "\f02d"; }
+.icon-bookmark:before             { content: "\f02e"; }
+.icon-print:before                { content: "\f02f"; }
+
+.icon-camera:before               { content: "\f030"; }
+.icon-font:before                 { content: "\f031"; }
+.icon-bold:before                 { content: "\f032"; }
+.icon-italic:before               { content: "\f033"; }
+.icon-text-height:before          { content: "\f034"; }
+.icon-text-width:before           { content: "\f035"; }
+.icon-align-left:before           { content: "\f036"; }
+.icon-align-center:before         { content: "\f037"; }
+.icon-align-right:before          { content: "\f038"; }
+.icon-align-justify:before        { content: "\f039"; }
+.icon-list:before                 { content: "\f03a"; }
+.icon-indent-left:before          { content: "\f03b"; }
+.icon-indent-right:before         { content: "\f03c"; }
+.icon-facetime-video:before       { content: "\f03d"; }
+.icon-picture:before              { content: "\f03e"; }
+
+.icon-pencil:before               { content: "\f040"; }
+.icon-map-marker:before           { content: "\f041"; }
+.icon-adjust:before               { content: "\f042"; }
+.icon-tint:before                 { content: "\f043"; }
+.icon-edit:before                 { content: "\f044"; }
+.icon-share:before                { content: "\f045"; }
+.icon-check:before                { content: "\f046"; }
+.icon-move:before                 { content: "\f047"; }
+.icon-step-backward:before        { content: "\f048"; }
+.icon-fast-backward:before        { content: "\f049"; }
+.icon-backward:before             { content: "\f04a"; }
+.icon-play:before                 { content: "\f04b"; }
+.icon-pause:before                { content: "\f04c"; }
+.icon-stop:before                 { content: "\f04d"; }
+.icon-forward:before              { content: "\f04e"; }
+
+.icon-fast-forward:before         { content: "\f050"; }
+.icon-step-forward:before         { content: "\f051"; }
+.icon-eject:before                { content: "\f052"; }
+.icon-chevron-left:before         { content: "\f053"; }
+.icon-chevron-right:before        { content: "\f054"; }
+.icon-plus-sign:before            { content: "\f055"; }
+.icon-minus-sign:before           { content: "\f056"; }
+.icon-remove-sign:before          { content: "\f057"; }
+.icon-ok-sign:before              { content: "\f058"; }
+.icon-question-sign:before        { content: "\f059"; }
+.icon-info-sign:before            { content: "\f05a"; }
+.icon-screenshot:before           { content: "\f05b"; }
+.icon-remove-circle:before        { content: "\f05c"; }
+.icon-ok-circle:before            { content: "\f05d"; }
+.icon-ban-circle:before           { content: "\f05e"; }
+
+.icon-arrow-left:before           { content: "\f060"; }
+.icon-arrow-right:before          { content: "\f061"; }
+.icon-arrow-up:before             { content: "\f062"; }
+.icon-arrow-down:before           { content: "\f063"; }
+.icon-share-alt:before            { content: "\f064"; }
+.icon-resize-full:before          { content: "\f065"; }
+.icon-resize-small:before         { content: "\f066"; }
+.icon-plus:before                 { content: "\f067"; }
+.icon-minus:before                { content: "\f068"; }
+.icon-asterisk:before             { content: "\f069"; }
+.icon-exclamation-sign:before     { content: "\f06a"; }
+.icon-gift:before                 { content: "\f06b"; }
+.icon-leaf:before                 { content: "\f06c"; }
+.icon-fire:before                 { content: "\f06d"; }
+.icon-eye-open:before             { content: "\f06e"; }
+
+.icon-eye-close:before            { content: "\f070"; }
+.icon-warning-sign:before         { content: "\f071"; }
+.icon-plane:before                { content: "\f072"; }
+.icon-calendar:before             { content: "\f073"; }
+.icon-random:before               { content: "\f074"; }
+.icon-comment:before              { content: "\f075"; }
+.icon-magnet:before               { content: "\f076"; }
+.icon-chevron-up:before           { content: "\f077"; }
+.icon-chevron-down:before         { content: "\f078"; }
+.icon-retweet:before              { content: "\f079"; }
+.icon-shopping-cart:before        { content: "\f07a"; }
+.icon-folder-close:before         { content: "\f07b"; }
+.icon-folder-open:before          { content: "\f07c"; }
+.icon-resize-vertical:before      { content: "\f07d"; }
+.icon-resize-horizontal:before    { content: "\f07e"; }
+
+.icon-bar-chart:before            { content: "\f080"; }
+.icon-twitter-sign:before         { content: "\f081"; }
+.icon-facebook-sign:before        { content: "\f082"; }
+.icon-camera-retro:before         { content: "\f083"; }
+.icon-key:before                  { content: "\f084"; }
+.icon-cogs:before                 { content: "\f085"; }
+.icon-comments:before             { content: "\f086"; }
+.icon-thumbs-up:before            { content: "\f087"; }
+.icon-thumbs-down:before          { content: "\f088"; }
+.icon-star-half:before            { content: "\f089"; }
+.icon-heart-empty:before          { content: "\f08a"; }
+.icon-signout:before              { content: "\f08b"; }
+.icon-linkedin-sign:before        { content: "\f08c"; }
+.icon-pushpin:before              { content: "\f08d"; }
+.icon-external-link:before        { content: "\f08e"; }
+
+.icon-signin:before               { content: "\f090"; }
+.icon-trophy:before               { content: "\f091"; }
+.icon-github-sign:before          { content: "\f092"; }
+.icon-upload-alt:before           { content: "\f093"; }
+.icon-lemon:before                { content: "\f094"; }
+.icon-phone:before                { content: "\f095"; }
+.icon-check-empty:before          { content: "\f096"; }
+.icon-bookmark-empty:before       { content: "\f097"; }
+.icon-phone-sign:before           { content: "\f098"; }
+.icon-twitter:before              { content: "\f099"; }
+.icon-facebook:before             { content: "\f09a"; }
+.icon-github:before               { content: "\f09b"; }
+.icon-unlock:before               { content: "\f09c"; }
+.icon-credit-card:before          { content: "\f09d"; }
+.icon-rss:before                  { content: "\f09e"; }
+
+.icon-hdd:before                  { content: "\f0a0"; }
+.icon-bullhorn:before             { content: "\f0a1"; }
+.icon-bell:before                 { content: "\f0a2"; }
+.icon-certificate:before          { content: "\f0a3"; }
+.icon-hand-right:before           { content: "\f0a4"; }
+.icon-hand-left:before            { content: "\f0a5"; }
+.icon-hand-up:before              { content: "\f0a6"; }
+.icon-hand-down:before            { content: "\f0a7"; }
+.icon-circle-arrow-left:before    { content: "\f0a8"; }
+.icon-circle-arrow-right:before   { content: "\f0a9"; }
+.icon-circle-arrow-up:before      { content: "\f0aa"; }
+.icon-circle-arrow-down:before    { content: "\f0ab"; }
+.icon-globe:before                { content: "\f0ac"; }
+.icon-wrench:before               { content: "\f0ad"; }
+.icon-tasks:before                { content: "\f0ae"; }
+
+.icon-filter:before               { content: "\f0b0"; }
+.icon-briefcase:before            { content: "\f0b1"; }
+.icon-fullscreen:before           { content: "\f0b2"; }
+
+.icon-group:before                { content: "\f0c0"; }
+.icon-link:before                 { content: "\f0c1"; }
+.icon-cloud:before                { content: "\f0c2"; }
+.icon-beaker:before               { content: "\f0c3"; }
+.icon-cut:before                  { content: "\f0c4"; }
+.icon-copy:before                 { content: "\f0c5"; }
+.icon-paper-clip:before           { content: "\f0c6"; }
+.icon-save:before                 { content: "\f0c7"; }
+.icon-sign-blank:before           { content: "\f0c8"; }
+.icon-reorder:before              { content: "\f0c9"; }
+.icon-list-ul:before              { content: "\f0ca"; }
+.icon-list-ol:before              { content: "\f0cb"; }
+.icon-strikethrough:before        { content: "\f0cc"; }
+.icon-underline:before            { content: "\f0cd"; }
+.icon-table:before                { content: "\f0ce"; }
+
+.icon-magic:before                { content: "\f0d0"; }
+.icon-truck:before                { content: "\f0d1"; }
+.icon-pinterest:before            { content: "\f0d2"; }
+.icon-pinterest-sign:before       { content: "\f0d3"; }
+.icon-google-plus-sign:before     { content: "\f0d4"; }
+.icon-google-plus:before          { content: "\f0d5"; }
+.icon-money:before                { content: "\f0d6"; }
+.icon-caret-down:before           { content: "\f0d7"; }
+.icon-caret-up:before             { content: "\f0d8"; }
+.icon-caret-left:before           { content: "\f0d9"; }
+.icon-caret-right:before          { content: "\f0da"; }
+.icon-columns:before              { content: "\f0db"; }
+.icon-sort:before                 { content: "\f0dc"; }
+.icon-sort-down:before            { content: "\f0dd"; }
+.icon-sort-up:before              { content: "\f0de"; }
+
+.icon-envelope-alt:before         { content: "\f0e0"; }
+.icon-linkedin:before             { content: "\f0e1"; }
+.icon-undo:before                 { content: "\f0e2"; }
+.icon-legal:before                { content: "\f0e3"; }
+.icon-dashboard:before            { content: "\f0e4"; }
+.icon-comment-alt:before          { content: "\f0e5"; }
+.icon-comments-alt:before         { content: "\f0e6"; }
+.icon-bolt:before                 { content: "\f0e7"; }
+.icon-sitemap:before              { content: "\f0e8"; }
+.icon-umbrella:before             { content: "\f0e9"; }
+.icon-paste:before                { content: "\f0ea"; }
+.icon-lightbulb:before            { content: "\f0eb"; }
+.icon-exchange:before             { content: "\f0ec"; }
+.icon-cloud-download:before       { content: "\f0ed"; }
+.icon-cloud-upload:before         { content: "\f0ee"; }
+
+.icon-user-md:before              { content: "\f0f0"; }
+.icon-stethoscope:before          { content: "\f0f1"; }
+.icon-suitcase:before             { content: "\f0f2"; }
+.icon-bell-alt:before             { content: "\f0f3"; }
+.icon-coffee:before               { content: "\f0f4"; }
+.icon-food:before                 { content: "\f0f5"; }
+.icon-file-alt:before             { content: "\f0f6"; }
+.icon-building:before             { content: "\f0f7"; }
+.icon-hospital:before             { content: "\f0f8"; }
+.icon-ambulance:before            { content: "\f0f9"; }
+.icon-medkit:before               { content: "\f0fa"; }
+.icon-fighter-jet:before          { content: "\f0fb"; }
+.icon-beer:before                 { content: "\f0fc"; }
+.icon-h-sign:before               { content: "\f0fd"; }
+.icon-plus-sign-alt:before        { content: "\f0fe"; }
+
+.icon-double-angle-left:before    { content: "\f100"; }
+.icon-double-angle-right:before   { content: "\f101"; }
+.icon-double-angle-up:before      { content: "\f102"; }
+.icon-double-angle-down:before    { content: "\f103"; }
+.icon-angle-left:before           { content: "\f104"; }
+.icon-angle-right:before          { content: "\f105"; }
+.icon-angle-up:before             { content: "\f106"; }
+.icon-angle-down:before           { content: "\f107"; }
+.icon-desktop:before              { content: "\f108"; }
+.icon-laptop:before               { content: "\f109"; }
+.icon-tablet:before               { content: "\f10a"; }
+.icon-mobile-phone:before         { content: "\f10b"; }
+.icon-circle-blank:before         { content: "\f10c"; }
+.icon-quote-left:before           { content: "\f10d"; }
+.icon-quote-right:before          { content: "\f10e"; }
+
+.icon-spinner:before              { content: "\f110"; }
+.icon-circle:before               { content: "\f111"; }
+.icon-reply:before                { content: "\f112"; }
+.icon-github-alt:before           { content: "\f113"; }
+.icon-folder-close-alt:before     { content: "\f114"; }
+.icon-folder-open-alt:before      { content: "\f115"; }
diff --git a/resources/lightbeam/data/graph.js b/resources/lightbeam/data/graph.js
new file mode 100644
index 0000000..8bf8065
--- /dev/null
+++ b/resources/lightbeam/data/graph.js
@@ -0,0 +1,416 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+// Graph Visualization (one of 3 views: graph, clock, and list). This is way
+// too heavyweight for mobile right now.
+
+// Visualization of tracking data interconnections
+
+(function (visualizations, global) {
+"use strict";
+
+// Bunch of utilities related to UI elements.
+const graphNodeRadius = {
+  "graph": 12
+};
+
+// The graph is an emitter with a default size.
+var graph = new Emitter();
+visualizations.graph = graph;
+graph.name = "graph";
+var width = 750,
+  height = 750;
+var force, vis;
+var edges, nodes;
+
+// There are three phases for a visualization life-cycle:
+// init does initialization and receives the existing set of connections
+// connection notifies of a new connection that matches existing filter
+// remove lets the visualization know it is about to be switched out so it can clean up
+graph.on('init', onInit);
+// graph.on('connection', onConnection);
+graph.on('remove', onRemove);
+graph.on('reset', onReset);
+
+/* for Highlighting and Colouring -------------------- */
+
+var highlight = {
+  visited: true,
+  neverVisited: true,
+  connections: true,
+  cookies: true,
+  watched: true,
+  blocked: true
+};
+
+// Restart the simulation. This is only called when there's a new connection we
+// haven't seen before.
+function onUpdate() {
+  // new nodes, reheat graph simulation
+  if (force) {
+    // console.log('restarting graph due to update');
+    force.stop();
+    force.nodes(filteredAggregate.nodes);
+    force.links(filteredAggregate.edges);
+    force.start();
+    updateGraph();
+    colourHighlightNodes(highlight);
+  } else {
+    console.log('the force is not with us');
+  }
+}
+
+function onInit() {
+  // console.log('graph::onInit()');
+  // console.log('initializing graph from %s connections', filteredAggregate.nodes.length);
+  // Handles all of the panning and scaling.
+  vis = d3.select(vizcanvas);
+  // A D3 visualization has a two main components, data-shaping, and setting up the D3 callbacks
+  // This binds our data to the D3 visualization and sets up the callbacks
+  initGraph();
+  aggregate.on('update', onUpdate);
+  // Differenct visualizations may have different viewBoxes, so make sure we use the right one
+  vizcanvas.setAttribute('viewBox', [0, 0, width, height].join(' '));
+  // console.log('graph::onInit end');
+  document.querySelector(".filter-display").classList.remove("hidden");
+}
+
+function onRemove() {
+  // var startTime = Date.now();
+  if (force) {
+    force.stop();
+    force = null;
+  }
+  resetCanvas();
+  document.querySelector(".filter-display").classList.add("hidden");
+  // console.log('it took %s ms to remove graph view', Date.now() - startTime);
+}
+
+function onReset() {
+  onRemove();
+  aggregate.emit('load', global.allConnections);
+}
+
+// UTILITIES FOR CREATING POLYGONS
+
+function point(angle, size) {
+  return [Math.round(Math.cos(angle) * size), -Math.round(Math.sin(angle) * size)];
+}
+
+function polygon(points, size, debug) {
+  var increment = Math.PI * 2 / points;
+  var angles = [],
+    i;
+  for (i = 0; i < points; i++) {
+    angles.push(i * increment + Math.PI / 2); // add 90 degrees so first point is up
+  }
+  return angles.map(function (angle) {
+    return point(angle, size);
+  });
+}
+
+function polygonAsString(points, size) {
+  var poly = polygon(points, size);
+  return poly.map(function (pair) {
+    return pair.join(',');
+  }).join(' ');
+}
+
+// ACCESSOR FUNCTIONS
+
+// function scaleNode(node){ return 'translate(' + node.x + ',' + node.y + ') scale(' + (1 + .05 * node.weight) + ')'; }
+function visited(node) {
+  return node.nodeType === 'site' || node.nodeType === 'both';
+}
+
+function notVisited(node) {
+  return node.nodeType === 'thirdparty';
+}
+// function timestamp(node){ return node.lastAccess.toISOString(); }
+// function nodeHighlight(node){ return ( node.visitedCount > 0 ) ? highlight.highlightVisited : highlight.highlightNeverVisited; }
+// function sourceX(edge){ return edge.source.x; }
+// function sourceY(edge){ return edge.source.y; }
+// function targetX(edge){ return edge.target.x; }
+// function targetY(edge){ return edge.target.y; }
+// function edgeCookie(edge){ return edge.cookieCount > 0; }
+// function edgeHighlight(edge){ return highlight.connections; }
+// function edgeColoured(edge){ return edge.cookieCount > 0 && highlight.cookies; }
+function nodeName(node) {
+  if (node) {
+    return node.name;
+  }
+  return undefined;
+}
+
+function siteHasPref(site, pref) {
+  return (userSettings.hasOwnProperty(site) &&
+    userSettings[site].contains(pref));
+}
+
+function watchSite(node) {
+  return siteHasPref(node.name, "watch");
+}
+
+function blockSite(node) {
+  return siteHasPref(node.name, "block");
+}
+
+// SET UP D3 HANDLERS
+
+var ticking = false;
+
+function charge(d) {
+  return -(500 + d.weight * 25);
+}
+
+function colourHighlightNodes(highlight) {
+  var i;
+  var watchedSites = document.querySelectorAll(".watched");
+  var blockedSites = document.querySelectorAll(".blocked");
+  if (highlight.watched) {
+    for (i = 0; i < watchedSites.length; i++) {
+      watchedSites[i].classList.add("watchedSites");
+    }
+  } else {
+    for (i = 0; i < watchedSites.length; i++) {
+      watchedSites[i].classList.remove("watchedSites");
+    }
+  }
+  if (highlight.blocked) {
+    for (i = 0; i < blockedSites.length; i++) {
+      blockedSites[i].classList.add("blockedSites");
+    }
+  } else {
+    for (i = 0; i < blockedSites.length; i++) {
+      blockedSites[i].classList.remove("blockedSites");
+    }
+  }
+}
+
+function initGraph() {
+  // Initialize D3 layout and bind data
+  // console.log('initGraph()');
+  force = d3.layout.force()
+    .nodes(filteredAggregate.nodes)
+    .links(filteredAggregate.edges)
+    .charge(charge)
+    .size([width, height])
+    .start();
+  updateGraph();
+  colourHighlightNodes(highlight);
+
+  // update method
+  var lastUpdate, lastTick;
+  lastUpdate = lastTick = Date.now();
+  var draws = [];
+  var ticks = 0;
+  const second = 1000;
+  const minute = 60 * second;
+  force.on('tick', function ontick(evt) {
+    // find a way to report how often tick() is called, and how long it takes to run
+    // without trying to console.log() every 5 milliseconds...
+    if (ticking) {
+      console.log('overlapping tick!');
+      return;
+    }
+    ticking = true;
+    var nextTick = Date.now();
+    ticks++;
+    lastTick = nextTick;
+    if ((lastTick - lastUpdate) > second) {
+      // console.log('%s ticks per second, each draw takes %s milliseconds', ticks, Math.floor(d3.mean(draws)));
+      lastUpdate = lastTick;
+      draws = [];
+      ticks = 0;
+    }
+    edges.each(function (d, i) {
+      // `this` is the DOM node
+      this.setAttribute('x1', d.source.x);
+      this.setAttribute('y1', d.source.y);
+      this.setAttribute('x2', d.target.x);
+      this.setAttribute('y2', d.target.y);
+      if (d.cookieCount) {
+        this.classList.add('cookieYes');
+      } else {
+        this.classList.remove('cookieYes');
+      }
+      if (highlight.connections) {
+        this.classList.add('highlighted');
+      } else {
+        this.classList.remove('highlighted');
+      }
+      if (d.cookieCount && highlight.cookies) {
+        this.classList.add('coloured');
+      } else {
+        this.classList.remove('coloured');
+      }
+    });
+    nodes.each(function (d, i) {
+      // `this` is the DOM node
+      this.setAttribute('transform', 'translate(' + d.x + ',' + d.y + ') scale(' + (1 + 0.05 * d.weight) + ')');
+      this.setAttribute('data-timestamp', d.lastAccess.toISOString());
+      if (d.nodeType === 'site' || d.nodeType === 'both') {
+        this.classList.add('visitedYes');
+        this.classList.remove('visitedNo');
+      } else {
+        this.classList.add('visitedNo');
+        this.classList.remove('visitedYes');
+      }
+      if ((d.nodeType === 'site' || d.nodeType === 'both') && highlight.visited) {
+        this.classList.add('highlighted');
+      } else if ((d.nodeType === 'thirdparty') && highlight.neverVisited) {
+        this.classList.add('highlighted');
+      } else {
+        this.classList.remove('highlighted');
+      }
+    });
+    var endDraw = Date.now();
+    draws.push(endDraw - lastTick);
+    if (force) {
+      nodes.call(force.drag);
+    }
+
+    ticking = false;
+  });
+}
+
+function updateGraph() {
+  // console.log('updateGraph()');
+  // Data binding for links
+  edges = vis.selectAll('.edge')
+    .data(filteredAggregate.edges, nodeName);
+
+  edges.enter().insert('line', ':first-child')
+    .classed('edge', true);
+
+  edges.exit()
+    .remove();
+
+  nodes = vis.selectAll('.node')
+    .data(filteredAggregate.nodes, nodeName);
+
+
+  nodes.enter().append('g')
+    .classed('visitedYes', visited)
+    .classed('visitedNo', notVisited)
+    .classed("watched", watchSite)
+    .classed("blocked", blockSite)
+    .call(addShape)
+    .attr('data-name', nodeName)
+    .on('mouseenter', tooltip.show)
+    .on('mouseleave', tooltip.hide)
+    .classed('node', true);
+
+  nodes.exit()
+    .remove();
+
+}
+
+function addFavicon(selection) {
+  selection.append("svg:image")
+    .attr("class", "favicon")
+    .attr("width", "16") // move these to the favicon class in css
+  .attr("height", "16")
+    .attr("x", "-8") // offset to make 16x16 favicon appear centered
+  .attr("y", "-8")
+    .attr("xlink:href", function (node) {
+      return 'http://' + node.name + '/favicon.ico';
+    });
+}
+
+function addCircle(selection) {
+  selection
+    .append('circle')
+    .attr('cx', 0)
+    .attr('cy', 0)
+    .attr('r', graphNodeRadius.graph)
+    .classed('site', true);
+}
+
+function addShape(selection) {
+  selection.filter('.visitedYes').call(addCircle).call(addFavicon);
+  selection.filter('.visitedNo').call(addTriangle).call(addFavicon);
+}
+
+function addTriangle(selection) {
+  selection
+    .append('polygon')
+    .attr('points', polygonAsString(3, 20))
+    .attr('data-name', function (node) {
+      return node.name;
+    });
+}
+
+
+
+// FIXME: Move this out of visualization so multiple visualizations can use it.
+function resetCanvas() {
+  // You will still need to remove timer events
+  var parent = vizcanvas.parentNode;
+  var newcanvas = vizcanvas.cloneNode(false);
+  var vizcanvasDefs = document.querySelector(".vizcanvas defs").cloneNode(true);
+  newcanvas.appendChild(vizcanvasDefs);
+  parent.replaceChild(newcanvas, vizcanvas);
+  vizcanvas = newcanvas;
+  aggregate.off('update', onUpdate);
+}
+
+
+
+var graphLegend = document.querySelector(".graph-footer");
+
+function legendBtnClickHandler(legendElm) {
+  legendElm.querySelector(".legend-controls").addEventListener("click", function (event) {
+    if (event.target.mozMatchesSelector(".btn, .btn *")) {
+      var btn = event.target;
+      while (btn.mozMatchesSelector('.btn *')) {
+        btn = btn.parentElement;
+      }
+      btn.classList.toggle("active");
+    }
+  });
+}
+
+legendBtnClickHandler(graphLegend);
+
+graphLegend.querySelector(".legend-toggle-visited").addEventListener("click", function (event) {
+  var visited = document.querySelectorAll(".visitedYes");
+  toggleVizElements(visited, "highlighted");
+  highlight.visited = !highlight.visited;
+});
+
+graphLegend.querySelector(".legend-toggle-never-visited").addEventListener("click", function (event) {
+  var neverVisited = document.querySelectorAll(".visitedNo");
+  toggleVizElements(neverVisited, "highlighted");
+  highlight.neverVisited = !highlight.neverVisited;
+});
+
+graphLegend.querySelector(".legend-toggle-connections").addEventListener("click", function (event) {
+  var cookiesConnections = document.querySelectorAll(".edge");
+  toggleVizElements(cookiesConnections, "highlighted");
+  highlight.connections = !highlight.connections;
+});
+
+graphLegend.querySelector(".legend-toggle-cookies").addEventListener("click", function (event) {
+  var cookiesConnections = document.querySelectorAll(".cookieYes");
+  toggleVizElements(cookiesConnections, "coloured");
+  highlight.cookies = !highlight.cookies;
+});
+
+graphLegend.querySelector(".legend-toggle-watched").addEventListener("click", function (event) {
+  highlight.watched = !highlight.watched;
+  colourHighlightNodes(highlight);
+});
+
+graphLegend.querySelector(".legend-toggle-blocked").addEventListener("click", function (event) {
+  highlight.blocked = !highlight.blocked;
+  colourHighlightNodes(highlight);
+});
+
+
+graphLegend.querySelector(".legend-toggle").addEventListener("click", function (event) {
+  toggleLegendSection(event.target, graphLegend);
+});
+
+
+})(visualizations, this);
diff --git a/resources/lightbeam/data/icons/lightbeam_150x45.png b/resources/lightbeam/data/icons/lightbeam_150x45.png
new file mode 100644
index 0000000..1e51947
Binary files /dev/null and b/resources/lightbeam/data/icons/lightbeam_150x45.png differ
diff --git a/resources/lightbeam/data/icons/lightbeam_icon_block.png b/resources/lightbeam/data/icons/lightbeam_icon_block.png
new file mode 100644
index 0000000..bfae0cf
Binary files /dev/null and b/resources/lightbeam/data/icons/lightbeam_icon_block.png differ
diff --git a/resources/lightbeam/data/icons/lightbeam_icon_empty_list.png b/resources/lightbeam/data/icons/lightbeam_icon_empty_list.png
new file mode 100644
index 0000000..c9d0440
Binary files /dev/null and b/resources/lightbeam/data/icons/lightbeam_icon_empty_list.png differ
diff --git a/resources/lightbeam/data/icons/lightbeam_icon_hide.png b/resources/lightbeam/data/icons/lightbeam_icon_hide.png
new file mode 100644
index 0000000..56af67b
Binary files /dev/null and b/resources/lightbeam/data/icons/lightbeam_icon_hide.png differ
diff --git a/resources/lightbeam/data/icons/lightbeam_icon_unblock.png b/resources/lightbeam/data/icons/lightbeam_icon_unblock.png
new file mode 100644
index 0000000..8dae32c
Binary files /dev/null and b/resources/lightbeam/data/icons/lightbeam_icon_unblock.png differ
diff --git a/resources/lightbeam/data/icons/lightbeam_icon_watch.png b/resources/lightbeam/data/icons/lightbeam_icon_watch.png
new file mode 100644
index 0000000..b28c9f9
Binary files /dev/null and b/resources/lightbeam/data/icons/lightbeam_icon_watch.png differ
diff --git a/resources/lightbeam/data/icons/lightbeam_logo-only_16x16.png b/resources/lightbeam/data/icons/lightbeam_logo-only_16x16.png
new file mode 100644
index 0000000..8049f50
Binary files /dev/null and b/resources/lightbeam/data/icons/lightbeam_logo-only_16x16.png differ
diff --git a/resources/lightbeam/data/icons/lightbeam_logo-only_32x32.png b/resources/lightbeam/data/icons/lightbeam_logo-only_32x32.png
new file mode 100644
index 0000000..d454fc2
Binary files /dev/null and b/resources/lightbeam/data/icons/lightbeam_logo-only_32x32.png differ
diff --git a/resources/lightbeam/data/icons/lightbeam_logo-only_48x48.png b/resources/lightbeam/data/icons/lightbeam_logo-only_48x48.png
new file mode 100644
index 0000000..137b2dc
Binary files /dev/null and b/resources/lightbeam/data/icons/lightbeam_logo-only_48x48.png differ
diff --git a/resources/lightbeam/data/icons/lightbeam_logo-wordmark_300x150.png b/resources/lightbeam/data/icons/lightbeam_logo-wordmark_300x150.png
new file mode 100644
index 0000000..d065b16
Binary files /dev/null and b/resources/lightbeam/data/icons/lightbeam_logo-wordmark_300x150.png differ
diff --git a/resources/lightbeam/data/image/Lightbeam---Wordmark-Beta.png b/resources/lightbeam/data/image/Lightbeam---Wordmark-Beta.png
new file mode 100644
index 0000000..757985c
Binary files /dev/null and b/resources/lightbeam/data/image/Lightbeam---Wordmark-Beta.png differ
diff --git a/resources/lightbeam/data/image/Lightbeam_radio_off.png b/resources/lightbeam/data/image/Lightbeam_radio_off.png
new file mode 100644
index 0000000..baf7da6
Binary files /dev/null and b/resources/lightbeam/data/image/Lightbeam_radio_off.png differ
diff --git a/resources/lightbeam/data/image/Lightbeam_radio_on.png b/resources/lightbeam/data/image/Lightbeam_radio_on.png
new file mode 100644
index 0000000..0148e76
Binary files /dev/null and b/resources/lightbeam/data/image/Lightbeam_radio_on.png differ
diff --git a/resources/lightbeam/data/image/lightbeam__wordmark_temp.png b/resources/lightbeam/data/image/lightbeam__wordmark_temp.png
new file mode 100644
index 0000000..23082d4
Binary files /dev/null and b/resources/lightbeam/data/image/lightbeam__wordmark_temp.png differ
diff --git a/resources/lightbeam/data/image/lightbeam_icon_about.png b/resources/lightbeam/data/image/lightbeam_icon_about.png
new file mode 100644
index 0000000..85ef02c
Binary files /dev/null and b/resources/lightbeam/data/image/lightbeam_icon_about.png differ
diff --git a/resources/lightbeam/data/image/lightbeam_icon_checkbox.png b/resources/lightbeam/data/image/lightbeam_icon_checkbox.png
new file mode 100644
index 0000000..3d1f0ac
Binary files /dev/null and b/resources/lightbeam/data/image/lightbeam_icon_checkbox.png differ
diff --git a/resources/lightbeam/data/image/lightbeam_icon_download2.png b/resources/lightbeam/data/image/lightbeam_icon_download2.png
new file mode 100644
index 0000000..655a5c5
Binary files /dev/null and b/resources/lightbeam/data/image/lightbeam_icon_download2.png differ
diff --git a/resources/lightbeam/data/image/lightbeam_icon_feedback.png b/resources/lightbeam/data/image/lightbeam_icon_feedback.png
new file mode 100644
index 0000000..715b41c
Binary files /dev/null and b/resources/lightbeam/data/image/lightbeam_icon_feedback.png differ
diff --git a/resources/lightbeam/data/image/lightbeam_icon_graph.png b/resources/lightbeam/data/image/lightbeam_icon_graph.png
new file mode 100644
index 0000000..0d8c2de
Binary files /dev/null and b/resources/lightbeam/data/image/lightbeam_icon_graph.png differ
diff --git a/resources/lightbeam/data/image/lightbeam_icon_help.png b/resources/lightbeam/data/image/lightbeam_icon_help.png
new file mode 100644
index 0000000..9200058
Binary files /dev/null and b/resources/lightbeam/data/image/lightbeam_icon_help.png differ
diff --git a/resources/lightbeam/data/image/lightbeam_icon_list.png b/resources/lightbeam/data/image/lightbeam_icon_list.png
new file mode 100644
index 0000000..75428ac
Binary files /dev/null and b/resources/lightbeam/data/image/lightbeam_icon_list.png differ
diff --git a/resources/lightbeam/data/image/lightbeam_icon_list_blue.png b/resources/lightbeam/data/image/lightbeam_icon_list_blue.png
new file mode 100644
index 0000000..9c16e17
Binary files /dev/null and b/resources/lightbeam/data/image/lightbeam_icon_list_blue.png differ
diff --git a/resources/lightbeam/data/image/lightbeam_icon_reset.png b/resources/lightbeam/data/image/lightbeam_icon_reset.png
new file mode 100644
index 0000000..cb44724
Binary files /dev/null and b/resources/lightbeam/data/image/lightbeam_icon_reset.png differ
diff --git a/resources/lightbeam/data/image/lightbeam_icon_sortby.png b/resources/lightbeam/data/image/lightbeam_icon_sortby.png
new file mode 100644
index 0000000..05d31b3
Binary files /dev/null and b/resources/lightbeam/data/image/lightbeam_icon_sortby.png differ
diff --git a/resources/lightbeam/data/image/lightbeam_icon_website.png b/resources/lightbeam/data/image/lightbeam_icon_website.png
new file mode 100644
index 0000000..93bf20f
Binary files /dev/null and b/resources/lightbeam/data/image/lightbeam_icon_website.png differ
diff --git a/resources/lightbeam/data/image/lightbeam_logo-only.svg b/resources/lightbeam/data/image/lightbeam_logo-only.svg
new file mode 100644
index 0000000..66cb8bd
--- /dev/null
+++ b/resources/lightbeam/data/image/lightbeam_logo-only.svg
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="234px" height="234px" viewBox="0 0 234 234" enable-background="new 0 0 234 234" xml:space="preserve">
+<g opacity="0.6">
+	
+		<radialGradient id="SVGID_1_" cx="117.3563" cy="207.1612" r="107.368" gradientTransform="matrix(1 1.493749e-04 -2.959144e-05 0.1733 0.0061 172.3689)" gradientUnits="userSpaceOnUse">
+		<stop  offset="0.3247" style="stop-color:#000000;stop-opacity:0.2"/>
+		<stop  offset="1" style="stop-color:#000000;stop-opacity:0"/>
+	</radialGradient>
+	<path fill="url(#SVGID_1_)" d="M221.107,200.784c5.545,1.113,5.545,2.918,0,4.031l-93.71,18.81c-5.545,1.113-14.536,1.113-20.081,0
+		l-93.71-18.81c-5.545-1.113-5.545-2.918,0-4.031l93.71-18.81c5.545-1.113,14.536-1.113,20.081,0L221.107,200.784z"/>
+	
+		<radialGradient id="SVGID_2_" cx="116.3563" cy="214.1114" r="56.8709" gradientTransform="matrix(1 1.493749e-04 -4.617017e-05 0.2703 0.0099 156.8797)" gradientUnits="userSpaceOnUse">
+		<stop  offset="0.2968" style="stop-color:#000000;stop-opacity:0.4"/>
+		<stop  offset="1" style="stop-color:#000000;stop-opacity:0"/>
+	</radialGradient>
+	<path opacity="0.8" fill="url(#SVGID_2_)" d="M221.107,200.784c5.545,1.113,5.545,2.918,0,4.031l-93.71,18.81
+		c-5.545,1.113-14.536,1.113-20.081,0l-93.71-18.81c-5.545-1.113-5.545-2.918,0-4.031l93.71-18.81
+		c5.545-1.113,14.536-1.113,20.081,0L221.107,200.784z"/>
+	
+		<radialGradient id="SVGID_3_" cx="116.8563" cy="219.4576" r="33.8761" gradientTransform="matrix(1 1.493749e-04 -5.262639e-05 0.3082 0.0116 152.1379)" gradientUnits="userSpaceOnUse">
+		<stop  offset="0.2968" style="stop-color:#000000;stop-opacity:0.4"/>
+		<stop  offset="1" style="stop-color:#000000;stop-opacity:0"/>
+	</radialGradient>
+	<path fill="url(#SVGID_3_)" d="M221.107,200.784c5.545,1.113,5.545,2.918,0,4.031l-93.71,18.81c-5.545,1.113-14.536,1.113-20.081,0
+		l-93.71-18.81c-5.545-1.113-5.545-2.918,0-4.031l93.71-18.81c5.545-1.113,14.536-1.113,20.081,0L221.107,200.784z"/>
+</g>
+<g>
+	
+		<radialGradient id="SVGID_4_" cx="633.8045" cy="1584.8099" r="198.3853" gradientTransform="matrix(0.7071 0.7071 -0.7071 0.7071 734.0817 -1396.0051)" gradientUnits="userSpaceOnUse">
+		<stop  offset="0.05" style="stop-color:#FFFFFF"/>
+		<stop  offset="0.2804" style="stop-color:#78C8E0"/>
+		<stop  offset="0.3424" style="stop-color:#68B7DD"/>
+		<stop  offset="0.4416" style="stop-color:#429BDB"/>
+		<stop  offset="0.6909" style="stop-color:#2B4D9F"/>
+		<stop  offset="0.7599" style="stop-color:#284083"/>
+		<stop  offset="0.9036" style="stop-color:#1D2855"/>
+		<stop  offset="0.9717" style="stop-color:#171E45"/>
+	</radialGradient>
+	<path fill="url(#SVGID_4_)" d="M220.917,104.799c5.554,5.554,5.554,14.558,0,20.111l-93.852,93.852
+		c-5.554,5.554-14.557,5.554-20.111,0L13.103,124.91c-5.554-5.554-5.554-14.558,0-20.111l93.852-93.852
+		c5.554-5.554,14.557-5.554,20.111,0L220.917,104.799z"/>
+	<g>
+		<defs>
+			<path id="SVGID_5_" d="M220.916,104.855c5.554,5.554,5.554,14.557,0,20.111l-93.85,93.85c-5.554,5.554-14.557,5.554-20.111,0
+				l-93.851-93.85c-5.554-5.554-5.554-14.557,0-20.111l93.851-93.85c5.553-5.554,14.557-5.554,20.111,0L220.916,104.855z"/>
+		</defs>
+		<clipPath id="SVGID_6_">
+			<use xlink:href="#SVGID_5_"  overflow="visible"/>
+		</clipPath>
+		<g clip-path="url(#SVGID_6_)">
+			<g>
+				<g>
+					<polygon fill="#FFFFFF" points="44.712,71.423 52.838,165.184 53.536,165.867 53.538,62.427 					"/>
+					<polygon fill="#FFFFFF" points="56.028,59.938 53.538,62.427 53.536,165.867 53.991,166.307 59.762,100.448 					"/>
+					<polygon fill="#FFFFFF" points="63.949,52.13 56.028,59.938 59.762,100.448 					"/>
+					<polygon fill="#FFFFFF" points="65.872,50.206 63.949,52.13 53.991,166.307 65.759,101.919 					"/>
+					<polygon fill="#FFFFFF" points="76.622,39.456 65.872,50.206 65.872,101.354 					"/>
+					<polygon fill="#FFFFFF" points="77.528,38.664 76.622,39.456 65.872,101.354 66.099,166.307 					"/>
+					<polygon fill="#FFFFFF" points="91.899,24.406 44.966,72.435 53.36,170.137 67.004,165.796 					"/>
+				</g>
+				<polygon fill="#FFFFFF" points="65.759,102.259 53.991,166.307 60.328,172.643 66.382,166.589 				"/>
+			</g>
+			<g>
+				<g>
+					<polygon fill="#FFFFFF" points="160.671,187.383 66.948,179.264 66.273,178.589 169.667,178.556 					"/>
+					<polygon fill="#FFFFFF" points="172.157,176.067 169.667,178.556 66.273,178.589 65.788,178.104 131.646,172.333 					"/>
+					<polygon fill="#FFFFFF" points="179.965,168.146 172.157,176.067 131.646,172.333 					"/>
+					<polygon fill="#FFFFFF" points="181.888,166.222 179.965,168.146 65.788,178.104 130.175,166.335 					"/>
+					<polygon fill="#FFFFFF" points="192.639,155.472 181.888,166.222 130.741,166.222 					"/>
+					<polygon fill="#FFFFFF" points="193.431,154.567 192.639,155.472 130.741,166.222 65.788,165.996 					"/>
+					<polygon fill="#FFFFFF" points="207.689,140.196 165.163,187.596 61.418,177.859 67.016,165.139 					"/>
+					<polyline fill="#FFFFFF" points="169.667,178.556 66.251,178.567 65.788,178.104 					"/>
+					<polyline fill="#FFFFFF" points="169.667,178.556 66.251,178.567 65.788,178.104 					"/>
+				</g>
+				<polygon fill="#FFFFFF" points="129.836,166.335 65.788,178.104 59.451,171.767 65.505,165.713 				"/>
+			</g>
+		</g>
+		<g opacity="0.4" clip-path="url(#SVGID_6_)">
+			<radialGradient id="SVGID_7_" cx="59.8482" cy="172.7286" r="160.9687" gradientUnits="userSpaceOnUse">
+				<stop  offset="0.1939" style="stop-color:#FFFFFF"/>
+				<stop  offset="0.4635" style="stop-color:#171E45;stop-opacity:0.5"/>
+			</radialGradient>
+			<path fill="url(#SVGID_7_)" d="M116.976,224.064c-4.148,0-8.047-1.615-10.981-4.548l-93.622-93.622
+				c-6.055-6.055-6.055-15.907,0-21.961l93.622-93.622c2.933-2.933,6.833-4.548,10.981-4.548c4.148,0,8.048,1.615,10.981,4.548
+				l93.622,93.622c6.055,6.055,6.055,15.907,0,21.961l-93.622,93.622C125.024,222.448,121.124,224.064,116.976,224.064z
+				 M116.976,8.447c-3.43,0-6.655,1.336-9.082,3.761l-93.622,93.622c-5.007,5.008-5.007,13.156,0,18.163l93.622,93.622
+				c2.426,2.426,5.651,3.761,9.082,3.761c3.431,0,6.656-1.336,9.082-3.761l93.622-93.622c5.007-5.008,5.007-13.156,0-18.163
+				l-93.622-93.622C123.632,9.783,120.407,8.447,116.976,8.447z"/>
+		</g>
+		<g clip-path="url(#SVGID_6_)">
+			<g>
+				<path fill="#FFFFFF" d="M59.762,100.448l-5.771,65.858l-0.455-0.44l0.002-103.44l2.49-2.49L59.762,100.448z M65.872,50.206
+					l-1.924,1.924l-9.958,114.177l11.769-64.387L65.872,50.206z M77.528,38.664l-0.905,0.792l-10.75,61.898l0.226,64.953
+					L77.528,38.664z"/>
+				<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="68.3053" y1="166.3065" x2="68.3053" y2="24.4059">
+					<stop  offset="0.25" style="stop-color:#FFFFFF"/>
+					<stop  offset="1" style="stop-color:#429BDB;stop-opacity:0.7"/>
+				</linearGradient>
+				<path fill="url(#SVGID_8_)" d="M53.538,62.427l-0.002,103.44l-0.698-0.683l-8.126-93.761L53.538,62.427z M63.949,52.13
+					l-7.921,7.808l3.734,40.511L63.949,52.13z M76.622,39.456l-10.75,10.75v51.147L76.622,39.456z M91.899,24.406L77.528,38.664
+					L66.099,166.307l0.905-0.51L91.899,24.406z"/>
+			</g>
+			<polygon fill="#FFFFFF" points="65.759,102.259 53.991,166.307 60.328,172.643 66.382,166.589 			"/>
+		</g>
+		<g clip-path="url(#SVGID_6_)">
+			<path fill="#FFFFFF" d="M172.157,176.067l-2.49,2.49l-103.394,0.033l-0.485-0.485l65.858-5.771L172.157,176.067z
+				 M130.175,166.335l-64.387,11.768l114.177-9.958l1.924-1.924L130.175,166.335z M65.788,165.996l64.953,0.226l61.898-10.75
+				l0.792-0.905L65.788,165.996z"/>
+			<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="66.0756" y1="163.7892" x2="207.6886" y2="163.7892">
+				<stop  offset="0.25" style="stop-color:#FFFFFF"/>
+				<stop  offset="1" style="stop-color:#429BDB;stop-opacity:0.7"/>
+			</linearGradient>
+			<path fill="url(#SVGID_9_)" d="M160.671,187.383l-93.723-8.118l-0.675-0.675l103.394-0.033L160.671,187.383z M131.646,172.333
+				l40.511,3.734l7.808-7.921L131.646,172.333z M130.741,166.222h51.147l10.75-10.75L130.741,166.222z M67.016,165.139l-0.94,0.88
+				l127.355-11.453l14.258-14.371L67.016,165.139z"/>
+			<polygon fill="#FFFFFF" points="129.836,166.335 65.788,178.104 59.451,171.767 65.505,165.713 			"/>
+		</g>
+		<radialGradient id="SVGID_10_" cx="60.4155" cy="171.6791" r="40.495" gradientUnits="userSpaceOnUse">
+			<stop  offset="0.2756" style="stop-color:#FFFFFF"/>
+			<stop  offset="0.3503" style="stop-color:#FFFFFF;stop-opacity:0.8969"/>
+			<stop  offset="1" style="stop-color:#FFFFFF;stop-opacity:0"/>
+		</radialGradient>
+		<polygon clip-path="url(#SVGID_6_)" fill="url(#SVGID_10_)" points="60.416,131.184 61.085,163.174 66.75,131.683 62.407,163.384 
+			72.929,133.166 63.68,163.797 78.8,135.598 64.873,164.405 84.218,138.918 65.956,165.192 89.05,143.045 66.903,166.138 
+			93.177,147.877 67.69,167.221 96.497,153.295 68.297,168.414 98.929,159.165 68.711,169.687 100.412,165.344 68.92,171.01 
+			100.911,171.679 68.92,172.348 100.412,178.014 68.711,173.671 98.929,184.193 68.297,174.944 96.497,190.063 67.69,176.137 
+			93.177,195.481 66.903,177.22 89.05,200.313 65.956,178.166 84.218,204.44 64.873,178.953 78.8,207.76 63.68,179.561 
+			72.929,210.192 62.407,179.975 66.75,211.675 61.085,180.184 60.416,212.174 59.746,180.184 54.081,211.675 58.424,179.975 
+			47.902,210.192 57.151,179.561 42.031,207.76 55.958,178.953 36.613,204.44 54.875,178.166 31.781,200.313 53.928,177.22 
+			27.654,195.481 53.141,176.137 24.334,190.063 52.534,174.944 21.903,184.193 52.12,173.671 20.419,178.014 51.911,172.348 
+			19.921,171.679 51.911,171.01 20.419,165.344 52.12,169.687 21.903,159.165 52.534,168.414 24.334,153.295 53.141,167.221 
+			27.654,147.877 53.928,166.138 31.781,143.045 54.875,165.192 36.613,138.918 55.958,164.405 42.031,135.598 57.151,163.797 
+			47.902,133.166 58.424,163.384 54.081,131.683 59.746,163.174 		"/>
+	</g>
+</g>
+</svg>
diff --git a/resources/lightbeam/data/image/lightbeam_popup_blocked.png b/resources/lightbeam/data/image/lightbeam_popup_blocked.png
new file mode 100644
index 0000000..06bdb35
Binary files /dev/null and b/resources/lightbeam/data/image/lightbeam_popup_blocked.png differ
diff --git a/resources/lightbeam/data/image/lightbeam_popup_hidden.png b/resources/lightbeam/data/image/lightbeam_popup_hidden.png
new file mode 100644
index 0000000..fae9663
Binary files /dev/null and b/resources/lightbeam/data/image/lightbeam_popup_hidden.png differ
diff --git a/resources/lightbeam/data/image/lightbeam_popup_privacy.png b/resources/lightbeam/data/image/lightbeam_popup_privacy.png
new file mode 100644
index 0000000..39fd8d5
Binary files /dev/null and b/resources/lightbeam/data/image/lightbeam_popup_privacy.png differ
diff --git a/resources/lightbeam/data/image/lightbeam_popup_stopsharing2.png b/resources/lightbeam/data/image/lightbeam_popup_stopsharing2.png
new file mode 100644
index 0000000..db8cdf5
Binary files /dev/null and b/resources/lightbeam/data/image/lightbeam_popup_stopsharing2.png differ
diff --git a/resources/lightbeam/data/image/lightbeam_popup_warningreset.png b/resources/lightbeam/data/image/lightbeam_popup_warningreset.png
new file mode 100644
index 0000000..25e0470
Binary files /dev/null and b/resources/lightbeam/data/image/lightbeam_popup_warningreset.png differ
diff --git a/resources/lightbeam/data/image/lightbeam_popup_warningsharing.png b/resources/lightbeam/data/image/lightbeam_popup_warningsharing.png
new file mode 100644
index 0000000..3a5d61c
Binary files /dev/null and b/resources/lightbeam/data/image/lightbeam_popup_warningsharing.png differ
diff --git a/resources/lightbeam/data/index.html b/resources/lightbeam/data/index.html
new file mode 100644
index 0000000..87b4b65
--- /dev/null
+++ b/resources/lightbeam/data/index.html
@@ -0,0 +1,479 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8" />
+    <title>Lightbeam</title>
+    <link rel="stylesheet" type="text/css" href="OpenSans.css" />
+    <link rel="stylesheet" type="text/css" href="font-awesome.css" />
+    <link rel="stylesheet" type="text/css" href="style.css" />
+    <link rel="shortcut icon" type="image/png" href="icons/lightbeam_logo-only_32x32.png">
+</head>
+<body role="application">
+    <div class="main">
+        <aside class="controls" role="navigation">
+            <header>
+                <img class="logo" src="icons/lightbeam_150x45.png" />
+            </header>
+
+            <div class="section-header all-cap-header">VISUALIZATION</div>
+            <div class="btn_group visualization" role="menu">
+                <div data-list class="dropdown_options">
+                    <a data-value="Graph" role="menuitem" tabIndex="1" aria-label="Graph Visualization">
+                        <img src="image/lightbeam_icon_graph.png" />graph
+                    </a>
+                    <a data-value="List" role="menuitem" tabIndex="2" aria-label="List. Display data in tabular format.">
+                        <img src="image/lightbeam_icon_list.png" />list
+                    </a>
+                </div>
+            </div>
+            <div class="section-header all-cap-header">DATA</div>
+            <div class="btn">
+                <a class="download">
+                    <img src="image/lightbeam_icon_download2.png" />Save Data
+                </a>
+            </div>
+            <div class="btn">
+                <a class="reset-data">
+                    <img src="image/lightbeam_icon_reset.png" />Reset Data
+                </a>
+            </div>
+            <div class="links">
+                <img src="image/lightbeam_icon_feedback.png"> <a href="mailto:lightbeam-feedback at mozilla.org">Give Us Feedback</a><br />
+                <a href="http://www.mozilla.org/lightbeam" target="_blank">mozilla.org/lightbeam</a>
+            </div>
+        </aside>
+
+        <div class="content-flex">
+            <div class="top-bar">
+                <div class="stats-section">
+                    <section>
+                        <div class="all-cap-header">DATA GATHERED SINCE</div>
+                        <div class="stat date-gathered large-header"></div>
+                    </section>
+                    <section>
+                        <div class="all-cap-header">YOU HAVE VISITED</div>
+                        <div class="stat first-party-sites large-header"></div>
+                    </section>
+                    <section>
+                        <div class="all-cap-header">YOU HAVE CONNECTED WITH</div>
+                        <div class="stat third-party-sites large-header"></div>
+                    </section>
+                </div>
+                <div class="sharing-section">
+                    <div class="label all-cap-header">CONTRIBUTE DATA</div>
+                    <div class="toggle-btn share-btn">
+                        <label><input type="checkbox" name="" id="" />
+                            <div class="toggle-btn-innner">
+                                <div class="switch"></div>
+                                <div class="on-off-text">OFF</div>
+                            </div>
+                        </label>
+                    </div>
+                </div>
+            </div>
+        <div id="content">
+            <aside class="info">
+                <div class="holder">
+                <!-- Site Profile starts ================================= -->
+                    <div class="site-profile-content hidden">
+                        <div>
+                            <header class="title large-header"></header>
+                            <!--span class="blue-text">You are currently browsing this site</span-->
+                            <span class="all-cap-header"><b>First Access </b></span><span class="info-first-access">Date</span><br/>
+                            <span class="all-cap-header"><b>Last Access </b></span><span class="info-last-access">Date</span>
+                            <div class="pref-tag hidden">
+                                <br/>
+                                <img src="" />
+                                This site is currently
+                                <b><span></span></b>.
+                                <br/>
+                            </div>
+                            <div class="pref-action">
+                                <br/>
+                                <div class="btn">
+                                    <a class="unblock hidden">
+                                        <img src="icons/lightbeam_icon_unblock.png" />Unblock Site
+                                    </a>
+                                    <a class="block">
+                                        <img src="icons/lightbeam_icon_block.png" />Block Site
+                                    </a>
+                                </div>
+                            </div>
+                        <div class="map-section">
+                            <header class="blue-text large-header">Server Location</header>
+                            <div><span id="country"> </span></div>
+                            <!-- SVG world map code starts; based on BlankMap-World6,_compact.svg by Canuckguy et al. =========================  -->
+                            <object type="image/svg+xml" data="map.svg" class="world-map"></object>
+                        </div>
+                        </div>
+                        <div class="connections-list">
+                            <header class="large-header">
+                                Connected to <b class="num-connected-sites"></b> since first access.
+                            </header>
+                            <ul>
+                            </ul>
+                        </div>
+                    </div>
+                <!-- Site Profile ends ================================= -->
+                <!-- Help Sections starts ================================= -->
+                    <div class="help-content">
+                    <!-- Graph View Help -->
+                        <div class="graph-view-help hidden">
+                            <header class="large-header"><img src="image/lightbeam_icon_help.png" /> Visualization Help</header>
+                            <div>
+                                <div class="grey-label all-cap-header">Visualization</div> Graph View<br />
+                                <div class="grey-label all-cap-header">Best for</div> Seeing site relationships<br />
+                            </div>
+                            <section>
+                                <div class="blue-text all-cap-header">DESCRIPTION</div>
+                                <p>
+                                    In this Graph visualization, <svg class="legend-canvas-small" version="1.1" xmlns="http://www.w3.org/2000/svg"><circle class="visitedSites" cx="8" cy="8" r="6" /></svg> <b>circular nodes</b> are websites you have visited and <svg class="legend-canvas-small" version="1.1" xmlns="http://www.w3.org/2000/svg"><polygon class="unvisitedSites" points="0,14 7,2 14,14" /></svg> <b>triangular nodes</b> are third party sites.  Over time, this graph will become a  [...]
+                                </p>
+                                <p>
+                                    This graph may get complex quickly; to reduce the amount of information displayed, use <b>FILTERS</b> to help focus on a set of data.
+                                </p>
+                            </section>
+                            <section>
+                                <div class="blue-text all-cap-header">Features</div>
+                                <div>
+                                    <div><span class="feature-name">Zoom In + Out</span> - on scroll</div>
+                                    <div><span class="feature-name">Pan</span> - on click + drag</div>
+                                    <div><span class="feature-name">Tooltips</span> - on hover</div>
+                                    <div><span class="feature-name">Website Profile</span> - on click</div>
+                                </div>
+                            </section>
+                            <section>
+                                <div class="blue-text all-cap-header">Legends and Controls</div>
+                                <p>
+                                    These buttons serve as toggles to help <u>identify</u> various elements on the graph.
+                                </p>
+                                <p>
+                                    <ul>
+                                        <li><svg class="legend-canvas-small" version="1.1" xmlns="http://www.w3.org/2000/svg">
+                                        <line class="cookies" x1="14" y1="2" x2="2" y2="14" />
+                                    </svg> <span class="cookie-text"><b>Cookies</b></span> identify when a site has stored some data in your browser.</li>
+                                    </ul><br/>
+                                    When you set your site preferences:
+                                    <ul>
+                                        <li><img src="icons/lightbeam_icon_block.png" /><span class="block-text">Block</span>  means they are blocked from connecting to your browser.</li>
+                                        <li><img src="icons/lightbeam_icon_watch.png" /><span class="watch-text">Watch</span>  means they are highlighted.</li>
+                                    </ul>
+                                    These preferences can be set in the List Visualization.
+                                </p>
+                            </section>
+                        </div>
+                    <!-- List View Help -->
+                        <div class="list-view-help hidden">
+                            <header class="large-header"><img src="image/lightbeam_icon_help.png" /> Visualization Help</header>
+                            <div>
+                                <div class="grey-label all-cap-header">Visualization</div> List View<br />
+                                <div class="grey-label all-cap-header">Best for</div> Seeing a text database<br />
+                            </div>
+                            <section>
+                                <div class="blue-text all-cap-header">Description</div>
+                                <p>
+                                    In this list Visualization, you can see a list of all websites you have visited and sites you have not visited but still connect to you.  This view assists with sorting and filtering large numbers of sites.
+                                </p>
+                            </section>
+                            <section>
+                                <div class="blue-text all-cap-header">Features</div>
+                                <div>
+                                    <div><span class="feature-name"><img src="image/lightbeam_icon_checkbox.png" />  Checkboxes</span> - click + apply site preference</div>
+                                    <div><span class="feature-name"><img src="image/lightbeam_icon_sortby.png" /> Sort By</span> - click column heading to sort by Type, Preference, Website(A-Z), Date, Sites Connected</div>
+                                </div>
+                            </section>
+                            <section>
+                                <div class="blue-text all-cap-header">Legends and Controls</div>
+                                <p>
+                                    These buttons <u>apply actions</u> to specific sites.
+                                </p>
+                                <p>
+                                    When you set your site preferences:
+                                    <ul>
+                                        <li><img src="icons/lightbeam_icon_block.png" /><span class="block-text">Block</span>  means sites are blocked from connecting to your browser.</li>
+                                        <li><img src="icons/lightbeam_icon_hide.png" /><span class="hide-text">Hide</span>  means sites are not seen in the visualization.</li>
+                                        <li><img src="icons/lightbeam_icon_watch.png" /><span class="watch-text">Watch</span>  means sites are highlighted.</li>
+                                    </ul>
+                                </p>
+                            </section>
+                        </div>
+                    </div>
+                <!-- Help Sections ends ================================= -->
+                <!-- About Section starts ================================= -->
+                    <div class="about-content">
+                        <div class="graph-view-help">
+                            <header class="large-header"><img src="image/lightbeam_icon_about.png" /> About Lightbeam</header>
+                            <div>
+                                <div class="grey-label all-cap-header">Version</div> <span id="version-number"></span><br />
+                                <div class="grey-label all-cap-header">By</div> Mozilla Foundation<br />
+                            </div>
+                            <section>
+                                <div class="blue-text all-cap-header">About this Add-on</div>
+                                <p>
+                                    Using interactive visualizations, Lightbeam allows you to see the first and third party sites you interact with on the Web. As you browse, Lightbeam reveals the full depth of the Web today, including parts that are not transparent to the average user. Using three distinct interactive graphic representations — Graph, Clock and List — Lightbeam enables you to examine individual third parties over time and space, identify where they connect to your online [...]
+                                </p>
+                                <p>
+                                    Mozilla partnered with the Social + Interactive Media (SIM) Centre at Emily Carr University of Art + Design to produce the aesthetic and functional data visualizations for Lightbeam.  To read more about the collaboration please visit: <a href="http://www.simcentre.ca/">http://www.simcentre.ca/</a>.
+                                </p>
+                            </section>
+                            <section>
+                                <div class="blue-text all-cap-header">Lightbeam Privacy Notice</div>
+                                <p>
+                                    We care about your privacy. Lightbeam is a browser add-on that collects and helps you visualize third party requests on any site you visit. If you choose to send Lightbeam data to Mozilla (that’s us), our <a href="#mozillaprivacypolicy">privacy policy</a> describes how we handle that data.
+                                </p>
+                                <div class="blue-text all-cap-header">Things you should know</div>
+                                <ul class="bullet-form">
+                                    <li>
+                                        After you install Lightbeam, the add-on collects data to help you visualize third party requests when you visit sites.
+                                        <ul>
+                                            <li>When you visit a site and that site contacts a third party, Lightbeam collects the following type of data: Domains of the visited sites and third parties, the existence of cookies, and a rough timestamp of when the site was visited. To see a complete list, please visit <a href="https://github.com/mozilla/lightbeam/blob/master/doc/data_format.v1.1.md">here.</a></li>
+                                        </ul>
+                                    </li>
+                                    <li>
+                                        By default, data collected by Lightbeam remains in your browser and is not sent to us.
+                                    </li>
+                                    <li>
+                                        You can choose to contribute your Lightbeam data to us. Data from Lightbeam can help us and others to understand third party relationships on the web and promote further research in the field of online tracking and privacy.
+                                        <ul>
+                                            <li>If you do contribute Lightbeam data to us, your browser will send us your Lightbeam data (you can see a list of the kind of data involved <a href="https://github.com/mozilla/lightbeam/blob/master/doc/data_format.v1.1.md">here.</a>).
+ We will post your data along with data from others in an aggregated and open database in a manner which we believe minimizes your risk of being re-identified. Opening this data can help users and researchers make more informed decisions based on the collective information.
+                                            </li>
+                                            <li>
+                                                Clicking "Reset data" at any time will delete the data stored locally in your browser.
+                                            </li>
+                                            <li>
+                                                Uninstalling Lightbeam prevents collection of any further Lightbeam data.
+                                            </li>
+                                         </ul>
+                                    </li>
+                                </ul>
+                            </section>
+                            <section>
+                                <div class="blue-text all-cap-header" id="mozillaprivacypolicy">Mozilla Privacy Policy – Learn More</div>
+                                <div class="privacy-policy">
+                                        <header><b>Lightbeam Privacy</b></header>
+                                        <p>Your privacy is an important factor that Mozilla (that's us) considers in the development of each of our products and services. We are committed to being transparent and open and want you to know how we receive information about you, and what we do with that information once we have it.</p>
+                                    <header><b>What do we mean by "personal information?"</b></header>
+                                    <p>
+                                        For us, "personal information" means information which identifies you, like your name or email address.
+                                    </p>
+                                    <p>
+                                        Any information that falls outside of this is "non-personal information."
+                                    </p>
+                                    <p>
+                                        If we store your personal information with information that is  non-personal, we will consider the combination as personal information.  If we remove all personal information from a set of data then the  remaining is non-personal information.
+                                    </p>
+                                        <header><b>How do we learn information about you?
+                                        </b></header>
+                                        <p>
+                                            We learn information about you when:
+                                        </p>
+                                        <ul>
+                                            <li>
+                                                you give it to us directly (e.g., when you choose to send us crash reports from Firefox); we collect it automatically through our products and services  (e.g., when we check whether your version of Firefox is up to date);
+                                            </li>
+                                            <li>
+                                                we collect it automatically through our products and services  (e.g., when we check whether your version of Firefox is up to date);
+                                            </li>
+                                            <li>
+                                                someone else tells us information about you (e.g., Thunderbird works with your email providers to set up your account); or
+                                            </li>
+                                            <li>
+                                                when we try and understand more about you based on information  you've given to us (e.g., when we use your IP address to customize  language for some of our services).
+                                            </li>
+                                        </ul>
+                                        <br/><header><b>What do we do with your information once we have it?
+                                        </b></header>
+                                        <p>When you give us personal information, we will use it in the ways for  which you've given us permission. Generally, we use your information to help us provide and improve our products and services for you.</p>
+                                        <header><b>When do we share your information with others?</b></header>
+                                        <ul>
+                                            <li>When we have gotten your permission to share it.</li>
+                                            <li>For processing or providing products and services to you, but only  if those entities receiving your information are contractually  obligated to handle the data in ways that are approved by Mozilla.</li>
+                                            <li>When we are fulfilling our <a href="https://www.mozilla.org/en-US/about/manifesto/">mission of being open</a>.  We sometimes release information to make our products better and foster  an open web, but when we do so, we will remove your personal  information and try to disclose it in a way that minimizes the risk of  you being re-identified.</li>
+                                            <li>When the law requires it. We follow the law whenever we receive  requests about you from a government or related to a lawsuit. We'll  notify you when we're asked to hand over your personal information in  this way unless we're legally prohibited from doing so. When we receive  requests like this, we'll only release your personal information if we  have a good faith belief that the law requires us to do so. Nothing in  this policy is intende [...]
+                                            <li>When we believe it is necessary to prevent harm to you or someone  else. We will only share your information in this way if we have a good  faith belief that it is reasonably necessary to protect the rights,  property or safety of you, our other users, Mozilla or the public.</li>
+                                            <li>If our organizational structure or status changes (if we undergo a  restructuring, are acquired, or go bankrupt) we may pass your  information to a successor or affiliate.</li><br/>
+                                        </ul>
+                                        <header><b>How do we store and protect your personal information?
+                                        </b></header>
+                                       <p> We are committed to protecting your personal information once we have  it. We implement physical, business and technical security measures. Despite our efforts, if we learn of a security breach, we'll notify you  so that you can take appropriate protective steps.</p>
+                                       <p>We also don't want your personal information for any longer than we  need it, so we only keep it long enough to do what we collected it for.  Once we don't need it, we take steps to destroy it unless we are  required by law to keep it longer.</p>
+                                       <header><b>What else do we want you know?
+                                        </b></header>
+                                       <p>We're a global organization and our computers are in several  different places around the world. We also use service providers whose  computers may also be in various countries. This means that your  information might end up on one of those computers in another country,  and that country may have a different level of data protection regulation than yours. By giving us information, you consent to this  kind of transfer of your information. No matt [...]
+
+                                        <p>If you are under 13, we don't want your personal information, and you  must not provide it to us. If you are a parent and believe that your  child who is under 13 has provided us with personal information, please contact us at <a href="mailto:lightbeam-privacy at mozilla.org">lightbeam-privacy at mozilla.org</a> to have your child's information removed.</p>
+                                        <header><b>What if we change this policy?
+                                        </b></header>
+                                        <p>
+                                            We may need to change this policy and when we do, we'll notify you.
+                                        </p>
+                                </div>
+                                <!-- Lightbeam Privacy Policy ends -->
+                            </section>
+                            <section>
+                                <div class="blue-text all-cap-header">Contact</div>
+                                <p>
+                                    IRC channel is #lightbeam on irc.mozilla.org. <br/>
+                                    GitHub: <a href="https://github.com/mozilla/lightbeam" target="_blank">https://github.com/mozilla/lightbeam</a>
+                                </p>
+                            </section>
+                        </div>
+                    </div>
+                <!-- About Section starts ================================= -->
+                </div>
+            </aside>
+            <div class="info-panel-controls">
+                <ul>
+                    <li class="toggle-site-profile disabled"><img src="image/lightbeam_icon_website.png" /><i class="icon-chevron-right hidden"></i></li>
+                    <li class="toggle-help"><a class="help"><img src="image/lightbeam_icon_help.png" /><i class="icon-chevron-right hidden"></i></a></li>
+                    <li class="toggle-about"><a class="about"><img src="image/lightbeam_icon_about.png" /></a><i class="icon-chevron-right hidden"></i></li>
+                </ul>
+            </div>
+            <div class="stage-stack" role="main">
+              <div class="stage">
+                    <div class="filter-display hidden">
+                        <header></header>
+                        <div class="blue-text all-cap-header">Graph View</div>
+                    </div>
+                  <svg class="vizcanvas" width="100%" height="100%" viewBox="-350 -450 700 500">
+                      <defs>
+                        <radialGradient fy="50%" fx="50%" r="50%" cy="50%" cx="50%" id="selected-glow">
+                            <stop style="stop-color:rgb(255,255,255);stop-opacity:1" offset="0%"></stop>
+                            <stop style="stop-color:rgb(0,0,0);stop-opacity:0" offset="100%"></stop>
+                        </radialGradient>
+                        <radialGradient fy="50%" fx="50%" r="50%" cy="50%" cx="50%" id="connected-glow">
+                            <stop style="stop-color:#73A4B8;stop-opacity:1" offset="0%"></stop>
+                            <stop style="stop-color:#73A4B8;stop-opacity:0.5" offset="40%"></stop>
+                            <stop style="stop-color:rgb(0,0,0);stop-opacity:0" offset="100%"></stop>
+                        </radialGradient>
+                      </defs>
+                  </svg>
+              </div><!-- .stage -->
+              <!-- LEGEND & CONTROLS for Graph ================================  -->
+              <div class="graph-footer hidden">
+                    <section class="legend-header">
+                        <div><header class="blue-text all-cap-header">TOGGLE CONTROLS</header></div>
+                        <div>
+                            <span class="blue-text all-cap-header">Filter</span>
+                            <a class="legend-toggle blue-text">Hide</a>
+                        </div>
+                    </section>
+                    <section class="legend-controls">
+                        <section class="column">
+                            <div class="btn legend-toggle-visited active">
+                                <a>
+                                    <svg class="legend-canvas-small" version="1.1" xmlns="http://www.w3.org/2000/svg">
+                                        <circle class="visitedSites" cx="8" cy="8" r="6" />
+                                    </svg>
+                                    Visited Sites
+                                </a>
+                            </div>
+                            <div class="btn legend-toggle-never-visited active">
+                                <a>
+                                    <svg class="legend-canvas-small" version="1.1" xmlns="http://www.w3.org/2000/svg">
+                                        <polygon class="unvisitedSites" points="0,14 7,2 14,14" />
+                                    </svg>
+                                    Third Party Sites
+                                </a>
+                            </div>
+                            <div class="btn legend-toggle-connections active">
+                                <a>
+                                    <svg class="legend-canvas-small" version="1.1" xmlns="http://www.w3.org/2000/svg">
+                                        <line class="connectionLine" x1="14" y1="2" x2="2" y2="14" />
+                                    </svg>
+                                    Connections
+                                </a>
+                            </div>
+                        </section>
+                        <section class="column">
+                            <div class="btn legend-toggle-watched active">
+                                <a>
+                                    <svg class="legend-canvas-large" version="1.1" xmlns="http://www.w3.org/2000/svg">
+                                        <circle class="watchedSites" cx="8" cy="8" r="6" />
+                                        <polygon class="watchedSites" points="18,14 25,2 32,14" />
+                                    </svg>
+                                    Watched Sites
+                                </a>
+                            </div>
+                            <div class="btn legend-toggle-blocked active">
+                                <a>
+                                    <svg class="legend-canvas-large" version="1.1" xmlns="http://www.w3.org/2000/svg">
+                                        <circle class="blockedSites" cx="8" cy="8" r="6" />
+                                        <line class="blockedSites" x1="11" y1="3" x2="4.5" y2="14" />
+                                        <polygon class="blockedSites" points="18,14 25,2 32,14" />
+                                        <line class="blockedSites" x1="27.5" y1="4.5" x2="23" y2="14" />
+                                    </svg>
+                                    Blocked Sites
+                                </a>
+                            </div>
+                        </section>
+                        <section class="column">
+                            <div class="btn legend-toggle-cookies active">
+                                <a>
+                                    <svg class="legend-canvas-small" version="1.1" xmlns="http://www.w3.org/2000/svg">
+                                        <line class="cookies" x1="14" y1="2" x2="2" y2="14" />
+                                    </svg>
+                                    Cookies
+                                </a>
+                            </div>
+                        </section>
+                        <div class="btn_group session">
+                            <div data-list class="dropdown_options align-right">
+                                <a data-value="recent">recent site</a>
+                                <a data-value="last10sites">last 10 sites</a>
+                                <a data-value="daily">daily</a>
+                                <a data-value="weekly">weekly</a>
+                            </div>
+                        </div>
+                    </section>
+                </div>
+              <!-- SITE PREFERENCES for List ================================  -->
+              <div class="list-footer">
+                    <section class="legend-header">
+                        <div><header class="blue-text all-cap-header">SITE PREFERENCES</header></div>
+                        <div><a class="legend-toggle blue-text">Hide</a></div>
+                    </section>
+                    <section class="legend-controls">
+                        <section class="align-left">
+                            <input id="block-pref" type="radio" name="pref-options" value="" >
+                               <label for="block-pref" class="btn disabled">
+                                    <div class="radio-dot"></div>
+                                    <img src="icons/lightbeam_icon_block.png" />
+                                    Block Site
+                                </label>
+                            <input id="hide-pref" type="radio" name="pref-options" value="">
+                                <label for="hide-pref" class="btn disabled">
+                                    <div class="radio-dot"></div>
+                                    <img src="icons/lightbeam_icon_hide.png" />
+                                    Hide Site
+                                </label>
+                            <input id="watch-pref" type="radio" name="pref-options" value="">
+                               <label for="watch-pref" class="btn disabled">
+                                    <div class="radio-dot"></div>
+                                    <img src="icons/lightbeam_icon_watch.png" />
+                                    Watch Site
+                               </label> 
+                            <input id="no-pref" type="radio" name="pref-options" value="">
+                               <label for="no-pref" class="btn disabled">
+                                    <div class="radio-dot"></div>
+                                    Clear Preference
+                               </label>
+                        </section>
+                        <section class="align-right">
+                            <div class="toggle-hidden btn">
+                                <a>
+                                    Hide Hidden Sites
+                                </a>
+                            </div>
+                        </section>
+                    </section>
+              </div>
+            </div><!-- .stage-stack -->
+        </div>
+    </div>
+    </div>
+    <div id="tooltip"></div>
+
+</body>
+</html>
diff --git a/resources/lightbeam/data/infobar.js b/resources/lightbeam/data/infobar.js
new file mode 100644
index 0000000..c7bf1d3
--- /dev/null
+++ b/resources/lightbeam/data/infobar.js
@@ -0,0 +1,362 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+(function (global) {
+// Used for managing the DOM for infobar part of the page
+'use strict';
+
+var g = global;
+global.initMap = function initMap(mapcanvas, mapDocument) {
+  var onDragMap = false;
+  var mapDragStart = {};
+
+  var oriMapViewBox = mapcanvas.getAttribute('viewBox');
+
+  // update info when clicking on a node in the graph visualization
+  document.querySelector('#content').addEventListener('click', function (event) {
+    // click could happen on .node or an element inside of .node
+    if (event.target.mozMatchesSelector('.node, .node *')) {
+      var node = event.target;
+      var name;
+      if (node.mozMatchesSelector('[type=checkbox], td [type=checkbox]')) return;
+      while (node.mozMatchesSelector('.node *')) {
+        node = node.parentElement;
+      }
+      if (node.dataset && node.dataset.isBlocked) {
+        return;
+      }
+      name = node.getAttribute("data-name");
+      selectedNodeEffect(name);
+      updateInfo(name);
+    }
+  }, false);
+
+  document.querySelector(".connections-list ul").addEventListener("click", function (event) {
+    var name = event.target.textContent;
+    var previouslySelected = document.querySelector(".connections-list ul li[data-selected]");
+    if (previouslySelected) {
+      document.querySelector(".connections-list ul li[data-selected]").removeAttribute("data-selected");
+    }
+    event.target.setAttribute("data-selected", true);
+    resetAllGlow("connected");
+    connectedNodeEffect(name);
+  });
+  var currentRequest;
+  // get server info from http://freegeoip.net
+  function getServerInfo(nodeName, callback) {
+    var info = parseUri(nodeName); // uses Steven Levithan's parseUri 1.2.2
+    var jsonURL = "http://freegeoip.net/json/" + info.host;
+    var request = new XMLHttpRequest();
+    currentRequest = info.host;
+    request.open("GET", jsonURL, true);
+    request.onload = function () {
+      if (currentRequest === info.host) {
+        callback((request.status === 200) ? JSON.parse(request.responseText) : false);
+      }
+    };
+    request.send(null);
+  }
+
+  // reset map
+  function resetMap() {
+    var preHighlight = mapDocument.querySelectorAll(".highlight-country");
+    if (preHighlight) {
+      toArray(preHighlight).forEach(function (element) {
+        element.classList.remove("highlight-country");
+      });
+    }
+    mapcanvas.setAttribute("viewBox", oriMapViewBox);
+  }
+
+  // update map
+  function updateMap(countryCode) {
+    var countryOnMap = mapcanvas.getElementById(countryCode);
+    if (!countryOnMap) {
+      console.log('no country found for country code "%s"', countryCode);
+      return;
+    }
+    countryOnMap.classList.add('highlight-country');
+
+    // position the highlighted country in center
+    var svgViewBox = mapcanvas.getAttribute("viewBox").split(" ");
+    var worldDimen = mapcanvas.getBoundingClientRect();
+    var countryDimen = countryOnMap.getBoundingClientRect();
+
+    var ratio = svgViewBox[2] / worldDimen.width;
+    var worldCenter = {
+      x: 0.5 * worldDimen.width + worldDimen.left,
+      y: 0.5 * worldDimen.height + worldDimen.top
+    };
+    var countryCenter = {
+      x: 0.5 * countryDimen.width + countryDimen.left,
+      y: 0.5 * countryDimen.height + countryDimen.top
+    };
+
+    var newViewBox = {
+      x: (countryCenter.x - worldCenter.x) * ratio,
+      y: (countryCenter.y - worldCenter.y) * ratio,
+      w: svgViewBox[2],
+      h: svgViewBox[3]
+    };
+    setZoom(newViewBox, mapcanvas);
+  }
+
+  document.querySelector(".pref-action .block").addEventListener('click', function (evt) {
+    var site = this.dataset.siteName;
+    confirmBlockSitesDialog(function (confirmed) {
+      if (confirmed) {
+        userSettings[site] = 'block';
+        global.self.port.emit('updateBlocklist', site, true);
+        showSitePref(site);
+      }
+    });
+    evt.preventDefault();
+  });
+
+  document.querySelector(".pref-action .unblock").addEventListener('click', function (evt) {
+    var site = this.dataset.siteName;
+    userSettings[site] = '';
+    global.self.port.emit('updateBlocklist', site, false);
+    showSitePref(site);
+    evt.preventDefault();
+  });
+
+  // updates info on the info panel
+  function updateInfo(nodeName) {
+
+    // get server info and then update content on the info panel
+    getServerInfo(nodeName, function (data) {
+      var nodeList = aggregate.nodeForKey(nodeName);
+      showFavIcon(nodeName);
+      showFirstAndLastAccess(nodeList[nodeName]);
+      showSitePref(nodeName);
+      showConnectionsList(nodeName, nodeList);
+      // display site profile in Info Panel 
+      showSiteProfile();
+      // update map after we have loaded the SVG
+      showServerLocation(data);
+    });
+
+  }
+
+  function showFavIcon(nodeName) {
+    var title = document.querySelector('.holder .title');
+    while (title.childNodes.length) {
+      title.removeChild(title.firstChild);
+    }
+    title.appendChild(elem(nodeName, {
+      src: 'http://' + nodeName + '/favicon.ico',
+      'class': 'favicon'
+    }));
+    title.appendChild(document.createTextNode(nodeName));
+  }
+
+  function showFirstAndLastAccess(site) {
+    var firstAccess = formattedDate(site.firstAccess, "long");
+    var lastAccess = formattedDate(site.lastAccess, "long");
+    document.querySelector('.info-first-access').textContent = firstAccess;
+    document.querySelector('.info-last-access').textContent = lastAccess;
+  }
+
+  function showSitePref(nodeName) {
+    var prefTag = document.querySelector(".pref-tag");
+    var blockButton = document.querySelector(".pref-action .block");
+    var unblockButton = document.querySelector(".pref-action .unblock");
+    var sitePref = userSettings[nodeName];
+    if (sitePref) {
+      prefTag.querySelector("img").src = "icons/lightbeam_icon_" + sitePref + ".png";
+      prefTag.querySelector("span").className = "";
+      prefTag.querySelector("span").classList.add(sitePref + "-text");
+      prefTag.querySelector("span").textContent = (sitePref == "hide") ? "hidden" : sitePref + "ed";
+      prefTag.classList.remove("hidden");
+      if (sitePref == "block") {
+        unblockButton.classList.remove("hidden");
+        blockButton.classList.add("hidden");
+      } else {
+        unblockButton.classList.add("hidden");
+        blockButton.classList.remove("hidden");
+      }
+    } else {
+      prefTag.classList.add("hidden");
+      unblockButton.classList.add("hidden");
+      blockButton.classList.remove("hidden");
+    }
+    unblockButton.dataset.siteName = nodeName;
+    blockButton.dataset.siteName = nodeName;
+  }
+
+  function showConnectionsList(nodeName, nodeList) {
+    var htmlList = elem('ul');
+    var numConnectedSites = 0;
+    for (var key in nodeList) {
+      if (key != nodeName) { // connected site
+        htmlList.appendChild(elem('li', {}, key));
+        numConnectedSites++;
+      }
+    }
+    document.querySelector(".num-connected-sites").textContent = numConnectedSites + " " + singularOrPluralNoun(numConnectedSites, "site");
+
+    var list = document.querySelector(".connections-list");
+    list.removeChild(list.querySelector('ul'));
+    list.appendChild(htmlList);
+  }
+
+  function showServerLocation(serverData) {
+    if (!serverData || serverData.country_name === "Reserved") {
+      document.querySelector("#country").textContent = "(Unable to find server location)";
+      resetMap();
+    } else {
+      // update country info only when it is different from the current one
+      if (serverData.country_name !== document.querySelector("#country").textContent) {
+        resetMap();
+        document.querySelector("#country").textContent = serverData.country_name;
+        updateMap(serverData.country_code.toLowerCase());
+      }
+    }
+  }
+
+  function showSiteProfile() {
+    var siteProfileTab = document.querySelector(".toggle-site-profile");
+    var contentToBeShown = document.querySelector(".site-profile-content");
+    var infoPanelOpen = document.querySelector("#content").classList.contains("showinfo");
+    var siteProfileTabActive = document.querySelector(".toggle-site-profile").classList.contains("active");
+    if (!infoPanelOpen) {
+      document.querySelector("#content").classList.add("showinfo");
+      showInfoPanelTab(siteProfileTab, contentToBeShown);
+    }
+
+    if (infoPanelOpen) {
+      if (!siteProfileTabActive) {
+        // make the previously active tab inactive
+        deactivatePreviouslyActiveTab();
+        showInfoPanelTab(siteProfileTab, contentToBeShown);
+      }
+    }
+
+    document.querySelector(".toggle-site-profile").classList.remove("disabled");
+  }
+
+
+  /* mapcanvas events */
+  mapcanvas.addEventListener("mousedown", function (event) {
+    onDragMap = true;
+    mapDragStart.x = event.clientX;
+    mapDragStart.y = event.clientY;
+  }, false);
+
+  mapcanvas.addEventListener("mousemove", function (event) {
+    if (onDragMap) {
+      mapcanvas.style.cursor = "-moz-grab";
+      var offsetX = (Math.ceil(event.clientX) - mapDragStart.x);
+      var offsetY = (Math.ceil(event.clientY) - mapDragStart.y);
+      var box = getZoom(mapcanvas);
+      box.x -= (offsetX * 10);
+      box.y -= (offsetY * 10);
+      mapDragStart.x += offsetX;
+      mapDragStart.y += offsetY;
+      setZoom(box, mapcanvas);
+    }
+
+  }, false);
+
+  mapcanvas.addEventListener("mouseup", function (event) {
+    onDragMap = false;
+    mapcanvas.style.cursor = "default";
+  }, false);
+
+  mapcanvas.addEventListener("mouseleave", function (event) {
+    onDragMap = false;
+    mapcanvas.style.cursor = "default";
+  }, false);
+
+  mapDocument.addEventListener("wheel", function (event) {
+    if (event.target.mozMatchesSelector(".mapcanvas, .mapcanvas *")) {
+      zoomWithinLimit(event, mapcanvas, mapZoomInLimit, mapZoomOutLimit);
+    }
+  }, false);
+
+
+};
+
+
+/* Info Panel Tabs ======================================== */
+
+
+/* Toggle Site Profile */
+document.querySelector(".toggle-site-profile").addEventListener("click", function () {
+  var tabClicked = document.querySelector(".toggle-site-profile");
+  if (!tabClicked.classList.contains("disabled")) {
+    var contentToBeShown = document.querySelector(".site-profile-content");
+    toggleInfoPanelTab(tabClicked, contentToBeShown);
+  }
+});
+
+/* Toggle About */
+document.querySelector(".toggle-about").addEventListener("click", function () {
+  var tabClicked = document.querySelector(".toggle-about");
+  var contentToBeShown = document.querySelector(".about-content");
+  toggleInfoPanelTab(tabClicked, contentToBeShown);
+});
+
+/* Toggle Help Sections */
+document.querySelector(".toggle-help").addEventListener("click", function () {
+  var tabClicked = document.querySelector(".toggle-help");
+  var contentToBeShown = document.querySelector(".help-content ." + g.currentVisualization.name + "-view-help");
+  toggleInfoPanelTab(tabClicked, contentToBeShown);
+});
+
+
+
+function toggleInfoPanelTab(tabClicked, contentToBeShown) {
+  var infoPanelOpen = document.querySelector("#content").classList.contains("showinfo");
+  var isActiveTab = tabClicked.classList.contains("active");
+  if (infoPanelOpen) {
+    if (isActiveTab) { // collapse info panel
+      document.querySelector("#content").classList.remove("showinfo");
+      tabClicked.classList.remove("active");
+      tabClicked.querySelector("img").classList.remove("hidden");
+      tabClicked.querySelector("i").classList.add("hidden");
+    } else {
+      // make the previously active tab inactive
+      deactivatePreviouslyActiveTab();
+      // make the selected tab active
+      showInfoPanelTab(tabClicked, contentToBeShown);
+    }
+  } else {
+    // open the info panel and make the selected tab active
+    document.querySelector("#content").classList.add("showinfo");
+    showInfoPanelTab(tabClicked, contentToBeShown);
+  }
+}
+
+
+function deactivatePreviouslyActiveTab() {
+  var previouslyActiveTab = document.querySelector(".info-panel-controls ul li.active");
+  if (previouslyActiveTab) {
+    previouslyActiveTab.classList.remove("active");
+    previouslyActiveTab.querySelector("img").classList.remove("hidden");
+    previouslyActiveTab.querySelector("i").classList.add("hidden");
+  }
+}
+
+
+// make the selected tab active
+function showInfoPanelTab(tabClicked, contentToBeShown) {
+  tabClicked.classList.add("active");
+  tabClicked.querySelector("img").classList.add("hidden");
+  tabClicked.querySelector("i").classList.remove("hidden");
+  hideAllInfoPanelContentExcept(contentToBeShown);
+}
+
+
+function hideAllInfoPanelContentExcept(elmToShow) {
+  document.querySelector(".site-profile-content").classList.add("hidden");
+  document.querySelector(".help-content .graph-view-help").classList.add("hidden");
+  document.querySelector(".help-content .list-view-help").classList.add("hidden");
+  document.querySelector(".about-content").classList.add("hidden");
+  if (elmToShow) {
+    elmToShow.classList.remove("hidden");
+  }
+}
+
+})(this);
diff --git a/resources/lightbeam/data/initialPage.js b/resources/lightbeam/data/initialPage.js
new file mode 100644
index 0000000..f3ef283
--- /dev/null
+++ b/resources/lightbeam/data/initialPage.js
@@ -0,0 +1,6 @@
+var openLightbeamButton = document.querySelector('button#openLightbeam');
+if (openLightbeamButton) {
+  openLightbeamButton.addEventListener('click', function () {
+    self.port.emit('openLightbeam', {});
+  });
+}
diff --git a/resources/lightbeam/data/lightbeam.js b/resources/lightbeam/data/lightbeam.js
new file mode 100644
index 0000000..05d943e
--- /dev/null
+++ b/resources/lightbeam/data/lightbeam.js
@@ -0,0 +1,178 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+(function (global) {
+
+'use strict';
+
+var visualizations = {};
+var currentFilter;
+var userSettings = {};
+var allConnections = [];
+var g = global;
+
+// Constants for indexes of properties in array format
+const SOURCE = 0;
+const TARGET = 1;
+const TIMESTAMP = 2;
+const CONTENT_TYPE = 3;
+const COOKIE = 4;
+const SOURCE_VISITED = 5;
+const SECURE = 6;
+const SOURCE_PATH_DEPTH = 7;
+const SOURCE_QUERY_DEPTH = 8;
+const SOURCE_SUB = 9;
+const TARGET_SUB = 10;
+const METHOD = 11;
+const STATUS = 12;
+const CACHEABLE = 13;
+const FROM_PRIVATE_MODE = 14;
+
+var vizcanvas = document.querySelector('.vizcanvas');
+var mapDocument, mapcanvas;
+document.querySelector('.world-map').addEventListener('load', function (event) {
+  mapDocument = event.target.contentDocument;
+  mapcanvas = mapDocument.querySelector('.mapcanvas');
+  initMap(mapcanvas, mapDocument);
+}, false);
+
+// Export everything
+global.visualizations = visualizations;
+global.currentFilter = currentFilter;
+global.userSettings = userSettings;
+global.vizcanvas = vizcanvas;
+global.allConnections = allConnections;
+
+// DOM Utility
+
+global.elem = function elem(name, attributes, children) {
+  // name is the tagName of an element
+  // [optional] attributes can be null or undefined, or an object of key/values to setAttribute on, attribute values can be functions to call to get the actual value
+  // [optional] children can be an element, text or an array (or null or undefined). If an array, can contain strings or elements
+  var e = document.createElement(name);
+  var val;
+  if (attributes && (Array.isArray(attributes) || attributes.nodeName || typeof attributes === 'string')) {
+    children = attributes;
+    attributes = null;
+  }
+  try {
+    if (attributes) {
+      Object.keys(attributes).forEach(function (key) {
+        if (attributes[key] === null || attributes[key] === undefined) return;
+        if (typeof attributes[key] === 'function') {
+          val = attributes[key](key, attributes);
+          if (val) {
+            e.setAttribute(key, val);
+          }
+        } else {
+          e.setAttribute(key, attributes[key]);
+        }
+      });
+    }
+  } catch (err) {
+    console.log('attributes: not what we think they are: %o', attributes);
+  }
+  if (children) {
+    if (!Array.isArray(children)) {
+      children = [children]; // convenience, allow a single argument vs. an array of one
+    }
+    children.forEach(function (child) {
+      if (child.nodeName) {
+        e.appendChild(child);
+      } else {
+        // assumes child is a string
+        e.appendChild(document.createTextNode(child));
+      }
+    });
+  }
+  return e;
+};
+
+window.addEventListener('load', function (evt) {
+  console.debug('window onload');
+  self.port.emit('uiready');
+  // Wire up events
+  document.querySelector('[data-value=Graph]').setAttribute("data-selected", true);
+  var visualizationName = "graph";
+  console.debug("current vis", visualizationName);
+  g.currentVisualization = visualizations[visualizationName];
+  switchVisualization(visualizationName);
+});
+
+function initCap(str) {
+  return str[0].toUpperCase() + str.slice(1);
+}
+
+global.switchVisualization = function switchVisualization(name) {
+  // var startTime = Date.now();
+  console.debug('switchVisualizations(' + name + ')');
+  if (g.currentVisualization != visualizations[name]) {
+    g.currentVisualization.emit('remove');
+  }
+  g.currentVisualization = visualizations[name];
+  resetAdditionalUI();
+  g.currentVisualization.emit('init');
+  self.port.emit("prefChanged", {
+    defaultVisualization: name
+  });
+  // console.log('it took %s ms to switch visualizations', Date.now() - startTime);
+};
+
+function resetAdditionalUI() {
+  // toggle off info panel
+  document.querySelector("#content").classList.remove("showinfo");
+  var activeTab = document.querySelector(".info-panel-controls ul li.active");
+  if (activeTab) { // make the active tab inactive, if any
+    activeTab.classList.remove("active");
+    activeTab.querySelector("img").classList.remove("hidden");
+    activeTab.querySelector("i").classList.add("hidden");
+  }
+  // hide all help sections
+  document.querySelector(".help-content .graph-view-help").classList.add("hidden");
+  document.querySelector(".help-content .list-view-help").classList.add("hidden");
+  // show vizcanvas again in case it is hidden
+  document.querySelector(".vizcanvas").classList.remove("hide");
+  // toggle footer section accordingly
+  document.querySelector(".graph-footer").classList.add("hidden");
+  document.querySelector(".list-footer").classList.add("hidden");
+  var vizName = g.currentVisualization.name;
+  document.querySelector("." + vizName + "-footer").classList.remove("hidden");
+}
+
+
+/****************************************
+ *   Format date string
+ */
+global.formattedDate = function formattedDate(date, format) {
+  var d = (typeof date == "number") ? new Date(date) : date;
+  var month = ["Jan", "Feb", "Mar", "Apr", "May", "June", "July", "Aug", "Sept", "Oct", "Nov", "Dec"][d.getMonth()];
+  var formatted = month + " " + d.getDate() + ", " + d.getFullYear();
+  if (format == "long") {
+    var dayInWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()];
+    formatted = dayInWeek + ", " + formatted + " " + ((d.getHours() == 12) ? 12 : (d.getHours() % 12)) + ':' + d.toLocaleFormat('%M') + ['AM', 'PM'][Math.floor(d.getHours() / 12)];
+  }
+  return formatted;
+};
+
+
+global.singularOrPluralNoun = function singularOrPluralNoun(num, str) {
+  if (typeof num != "number") {
+    num = parseFloat(num);
+  }
+  return (num !== 1) ? str + "s" : str;
+};
+
+/****************************************
+ *   update Stats Bar
+ */
+global.updateStatsBar = function updateStatsBar() {
+  var dateSince = "just now";
+  if (global.allConnections.length > 0) {
+    dateSince = formattedDate(global.allConnections[0][2]);
+  }
+  document.querySelector(".top-bar .date-gathered").textContent = dateSince;
+  document.querySelector(".top-bar .third-party-sites").textContent = aggregate.trackerCount + " " + singularOrPluralNoun(aggregate.trackerCount, "THIRD PARTY SITE");
+  document.querySelector(".top-bar .first-party-sites").textContent = aggregate.siteCount + " " + singularOrPluralNoun(aggregate.siteCount, "SITE");
+};
+
+})(this);
diff --git a/resources/lightbeam/data/list.js b/resources/lightbeam/data/list.js
new file mode 100644
index 0000000..663a0a9
--- /dev/null
+++ b/resources/lightbeam/data/list.js
@@ -0,0 +1,709 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+// List Visualization
+
+// Display data in tabular format
+
+(function (visualizations, global) {
+"use strict";
+
+var list = new Emitter();
+var breadcrumbStack = [];
+visualizations.list = list;
+list.name = "list";
+
+list.on("init", onInit);
+// list.on("connection", onConnection);
+list.on("remove", onRemove);
+list.on("showFilteredTable", function (filter) {
+  showFilteredTable(filter);
+});
+list.on('reset', onReset);
+
+function onReset() {
+  console.debug("reset list");
+  breadcrumbStack = [];
+  onRemove();
+  aggregate.emit('load', global.allConnections);
+}
+
+function onInit() {
+  // console.log('list::onInit()');
+  vizcanvas.classList.add("hide"); // we don't need vizcanvas here, so hide it
+  // A D3 visualization has a two main components, data-shaping, and setting up the D3 callbacks
+  // This binds our data to the D3 visualization and sets up the callbacks
+  initList();
+  initializeHandlers();
+  toggleShowHideHiddenButton();
+  aggregate.on('update', onUpdate);
+}
+
+function onUpdate() {
+  let {
+    nodes
+  } = aggregate;
+  let oldNodeRows = getAllRows().map(function (row) row.getAttribute('data-name'));
+  let newNodes = nodes.filter(function (node) {
+    return oldNodeRows.indexOf(node.name) < 0;
+  });
+  if (newNodes.length <= 0) {
+    return;
+  }
+  document.getElementById('refresh-data-link').textContent = 'Click here to refresh list...';
+  document.getElementById('refresh-data-row').classList.add('show');
+  return;
+}
+
+function onConnection(conn) {
+  var connection = aggregate.connectionAsObject(conn);
+}
+
+
+function onRemove() {
+  // console.log('removing list');
+  // var startTime = Date.now();
+  resetCanvas();
+  aggregate.off('update', onUpdate);
+  // console.log('It took %s ms to remove list view', Date.now() - startTime);
+}
+
+
+function initList() {
+  var stage = document.querySelector('.stage');
+
+  // breadcrumb
+  initBreadcrumb();
+
+  // add number of row selected label
+  var selectedLabel = elem("div", {
+    "class": "rows-selected-label blue-text"
+  }, [
+    elem("div", {
+      "class": "some-selected hidden"
+    }, [
+      elem("span", {
+        "class": "num-selected"
+      }),
+      " out of ",
+      elem("span", {
+        "class": "num-total"
+      }),
+      " sites selected",
+      elem("br"),
+      elem('span', {
+        'class': 'deselect'
+      }, 'clear all selected')
+    ]),
+    elem("div", {
+      "class": "none-selected"
+    }, [
+      elem("span", {
+        "class": "num-total"
+      }),
+      " sites"
+    ])
+  ]);
+  stage.appendChild(selectedLabel);
+
+  // list header
+  var table = elem("div", {
+    'class': 'list-table'
+  }, [
+    elem('table', {
+      'role': 'grid',
+      'aria-label': 'Entering List table'
+    }, [
+      elem('tr', {
+        'class': 'refresh',
+        'id': 'refresh-data-row'
+      }, [
+        elem('td', {
+          'colspan': '7',
+          'id': 'refresh-data-link'
+        })
+      ]),
+      elem('thead', {
+        'class': 'header-table'
+      }, [
+        elem('tr', {
+          'role': 'row',
+          'tabIndex': '0'
+        }, [
+          elem('th', elem('input', {
+            'class': 'selected-header',
+            type: 'checkbox',
+            'tabIndex': '-1'
+          })),
+          elem('th', {
+            'role': 'gridcell'
+          }, 'Type'),
+          elem('th', {
+            'role': 'gridcell'
+          }, 'Prefs'),
+          elem('th', {
+            'role': 'gridcell'
+          }, 'Website'),
+          elem('th', {
+            'role': 'gridcell'
+          }, 'First Access'),
+          elem('th', {
+            'role': 'gridcell'
+          }, 'Last Access'),
+          elem('th', {
+            'class': 'sort-numeric',
+            'role': 'gridcell'
+          }, 'Sites Connected')
+        ])
+      ]),
+    ]),
+    elem('div', {
+        'class': 'body-table'
+      },
+      elem('table', {
+          'role': 'grid'
+        },
+        elem('tbody', {
+          'class': 'list-body'
+        })
+      )
+    )
+  ]);
+  stage.appendChild(table);
+
+  showFilteredTable(); // showing all data so no filter param is passed here
+  updateBreadcrumb();
+}
+
+function initBreadcrumb() {
+  var stage = document.querySelector('.stage');
+  var breadcrumb = elem("div", {
+    "class": "breadcrumb"
+  });
+  stage.appendChild(breadcrumb);
+}
+
+function updateBreadcrumb(url) {
+  // push to breadcrumbStack
+  breadcrumbStack.push(url ? url : "All Sites");
+  // remove all child nodes in breadcrumb container before we start mapping breadcrumbs to UI again
+  resetVisibleBreadcrumb();
+  // map breadcrumbs to UI
+  mapBreadcrumbsToUI();
+}
+
+var breadcrumbClickHandler = function (event) {
+  var url = event.target.getAttribute("site-url");
+  var idxInStack = event.target.getAttribute("idx");
+  while (breadcrumbStack.length > idxInStack) {
+    breadcrumbStack.pop();
+  }
+  showFilteredTable(url);
+};
+
+function mapBreadcrumbsToUI() {
+  var breadcrumb = document.querySelector(".breadcrumb");
+  var lastIdxInStack = breadcrumbStack.length - 1;
+  // add "All Sites" to breadcrumb container
+  breadcrumb.appendChild(elem("div", {
+    "class": "breadcrumb-chunk"
+  }, breadcrumbStack[0]));
+  // other than "All Sites", there is only 1 tier in breadcrumbStack
+  // add that tier to breadcrumb container
+  if (lastIdxInStack == 1) {
+    breadcrumb.appendChild(elem("div", {
+      "class": "arrow-left"
+    }));
+    breadcrumb.appendChild(elem("div", {
+        "class": "breadcrumb-chunk no-click",
+        "site-url": breadcrumbStack[lastIdxInStack]
+      },
+      breadcrumbStack[lastIdxInStack]));
+  }
+  // other than "All Sites", there are more than 1 tier in breadcrumbStack
+  // we only want to show "All Sites" and the last 2 tiers
+  // so add the last 2 tiers to breadcrumb container
+  if (lastIdxInStack >= 2) {
+    // second last tier
+    breadcrumb.appendChild(elem("div", {
+      "class": "arrow-left"
+    }));
+    breadcrumb.appendChild(elem("div", {
+        "class": "breadcrumb-chunk",
+        "site-url": breadcrumbStack[lastIdxInStack - 1],
+        "idx": (lastIdxInStack - 1)
+      },
+      breadcrumbStack[lastIdxInStack - 1]));
+    // last tier
+    breadcrumb.appendChild(elem("div", {
+      "class": "arrow-left"
+    }));
+    breadcrumb.appendChild(elem("div", {
+        "class": "breadcrumb-chunk no-click",
+        "site-url": breadcrumbStack[lastIdxInStack],
+        "idx": lastIdxInStack
+      },
+      breadcrumbStack[lastIdxInStack]));
+  }
+
+  // add breadcrumbs click event handler
+  var allBreadcrumbChunks = document.querySelectorAll(".breadcrumb-chunk");
+  toArray(allBreadcrumbChunks).forEach(function (chunk) {
+    if (!chunk.classList.contains("no-click")) {
+      chunk.addEventListener("click", breadcrumbClickHandler, false);
+    }
+  });
+}
+
+function resetVisibleBreadcrumb() {
+  var breadcrumbContainer = document.querySelector(".breadcrumb");
+  while (breadcrumbContainer.firstChild) {
+    breadcrumbContainer.removeChild(breadcrumbContainer.firstChild);
+  }
+}
+
+function updateNumTotalRowsLabel() {
+  var numTotal = getAllRows().length;
+  var labels = document.querySelectorAll(".num-total");
+  for (var i = 0; i < labels.length; i++) {
+    labels[i].textContent = numTotal;
+  }
+}
+
+function updateRowSelectedLabel() {
+  var numSelected = getSelectedRows().length;
+  var selectedLabel = document.querySelector(".some-selected");
+  var noneSelectedLabel = document.querySelector(".none-selected");
+  if (numSelected > 0) {
+    selectedLabel.querySelector(".num-selected").textContent = numSelected;
+    selectedLabel.classList.remove("hidden");
+    noneSelectedLabel.classList.add("hidden");
+  } else {
+    selectedLabel.classList.add("hidden");
+    noneSelectedLabel.classList.remove("hidden");
+  }
+}
+
+function resetSelectedRows() {
+  let selectedRows = getSelectedRows();
+
+  for (let i = 0; i < selectedRows.length; i++) {
+    let sel = selectedRows[i];
+
+    sel.querySelector('.selected-row').checked = false;
+    sel.classList.remove('checked');
+  }
+
+  // Also uncheck the header input box if it's checked
+  document.querySelector('.selected-header').checked = false;
+
+  // Update the selected rows header to reflect the changes
+  updateRowSelectedLabel();
+}
+
+var lastFilter = null;
+
+function showFilteredTable(filter) {
+  console.debug("showFilteredTable", filter);
+  if (lastFilter != filter) updateBreadcrumb(filter);
+  lastFilter = filter;
+  // remove existing table tbodys, if any
+  var table = document.querySelector(".list-table");
+  var tbody = table.querySelector('.list-body');
+  var tbodyParent = tbody.parentElement;
+  tbodyParent.removeChild(tbody);
+  var nodes = getNodes(filter);
+  console.debug("getNodes", nodes);
+  tbodyParent.appendChild(createBody(nodes));
+  // update other UI elements
+  document.querySelector('.selected-header').checked = false;
+  updateNumTotalRowsLabel();
+  updateRowSelectedLabel();
+}
+
+
+function getNodes(filter) {
+  if (!filter) { // if no filter, show all
+    return aggregate.getAllNodes();
+  } else {
+    var nodeMap = aggregate.nodeForKey(filter);
+    return Object.keys(nodeMap).map(function (key) {
+      return nodeMap[key];
+    });
+  }
+}
+// A Node has the following properties:
+// contentTypes: []
+// cookieCount: #
+// firstAccess: Date
+// howMany: #
+// method: []
+// name: ""
+// nodeType: site | thirdparty | both
+// secureCount: #
+// status: []
+// subdomain: []
+// visitedCount: #
+
+
+function nodeToRow(node) {
+  var settings = userSettings[node.name] || (node.nodeType == 'blocked' ? 'block' : '');
+  var iconUrl = node.nodeType === 'blocked' ? 'icons/lightbeam_icon_empty_list.png' : 'image/lightbeam_icon_list.png';
+  var listIcon = elem('img', {
+    'src': iconUrl,
+    'class': node.nodeType === 'blocked' ? 'no-update' : 'update-table',
+    'role': 'gridcell'
+  });
+  var row = elem('tr', {
+    'class': 'node ' + node.nodeType,
+    'data-pref': settings,
+    'data-name': node.name,
+    'site-url': node.name,
+    'role': 'row',
+    'tabIndex': '0'
+  }, [
+    elem('td', elem('input', {
+      'type': 'checkbox',
+      'class': 'selected-row',
+      'tabIndex': '-1'
+    })),
+    elem('td', {
+      'data-sort-key': node.nodeType,
+      'role': 'gridcell'
+    }, node.nodeType === 'thirdparty' ? 'Third Party' : (node.nodeType === 'blocked' ? 'Unknown' : 'Visited')),
+    elem('td', {
+      'class': 'preferences',
+      'data-sort-key': settings,
+      'role': 'gridcell'
+    }, '\u00A0'),
+    elem('td', {
+      'data-sort-key': node.name,
+      'role': 'gridcell'
+    }, [
+      listIcon,
+      node.name
+    ]),
+    elem('td', {
+      'data-sort-key': node.firstAccess,
+      'role': 'gridcell'
+    }, (node.nodeType === 'blocked' ? 'Unknown' : formattedDate(node.firstAccess))),
+    elem('td', {
+      'data-sort-key': node.lastAccess,
+      'role': 'gridcell'
+    }, (node.nodeType === 'blocked' ? 'Unknown' : formattedDate(node.lastAccess))),
+    elem('td', {
+      'data-sort-key': aggregate.getConnectionCount(node),
+      'role': 'gridcell'
+    }, aggregate.getConnectionCount(node) + '')
+  ]);
+  if (node.nodeType !== 'blocked') {
+    listIcon.addEventListener("mouseenter", tooltip.addTooltip);
+    listIcon.addEventListener("mouseleave", tooltip.hide);
+    row.addEventListener("mouseenter", function () {
+      row.childNodes[3].firstChild.setAttribute("src", "image/lightbeam_icon_list_blue.png");
+    });
+    row.addEventListener("mouseleave", function () {
+      row.childNodes[3].firstChild.setAttribute("src", iconUrl);
+    });
+  }
+  if (node.nodeType === 'blocked') {
+    row.dataset.isBlocked = true;
+  }
+  return row;
+}
+
+
+function createBody(nodes) {
+  return elem("tbody", {
+    'class': 'list-body'
+  }, nodes.map(nodeToRow));
+}
+
+function sort(item1, item2) {
+  if (item1[0] < item2[0]) return -1;
+  if (item2[0] < item1[0]) return 1;
+  return 0;
+}
+
+function reverseSort(item1, item2) {
+  if (item1[0] < item2[0]) return 1;
+  if (item2[0] < item1[0]) return -1;
+  return 0;
+}
+
+function sortTableOnColumn(table, n) {
+  return function (evt) { // we could probably determine the column from the event.target
+    // if this is sorted column, reverse
+    // if this is reversed column, re-sort
+    // if this is not sorted column, unset sorted flag on that column
+    var reversed = evt.target.classList.contains('reverse-sorted');
+    var sorted = evt.target.classList.contains('sorted');
+
+    if (!(sorted || reversed)) {
+      var oldcolumn = table.querySelector('.sorted, .reverse-sorted');
+      if (oldcolumn) {
+        oldcolumn.classList.remove('sorted');
+        oldcolumn.classList.remove('reverse-sorted');
+      }
+    }
+    var tbody = table.querySelector('tbody');
+    var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr')).map(function (row) {
+      if (evt.target.classList.contains('sort-numeric')) {
+        return [parseInt(row.children[n].dataset.sortKey, 10), row];
+      } else {
+        return [row.children[n].dataset.sortKey, row];
+      }
+    });
+    if (sorted) {
+      evt.target.classList.remove('sorted');
+      evt.target.classList.add('reverse-sorted');
+      rows.sort(reverseSort);
+    } else {
+      evt.target.classList.remove('reverse-sorted');
+      evt.target.classList.add('sorted');
+      rows.sort(sort);
+    }
+
+    var frag = document.createDocumentFragment();
+    var preFrag = document.createDocumentFragment();
+
+    rows.forEach(function (row) {
+      var rowElement = row[1];
+
+      // Check if there are any preferences set for this row
+      var prefVal = rowElement.attributes.getNamedItem('data-pref').value;
+
+      if (prefVal) {
+        // This row is marked with a preference and should
+        // be appended to the top fragment.
+        preFrag.appendChild(rowElement);
+      } else {
+        frag.appendChild(rowElement);
+      }
+    });
+
+    tbody.appendChild(preFrag);
+    tbody.appendChild(frag);
+  };
+}
+
+function resetCanvas() {
+  var listTable = document.querySelector('.stage .list-table');
+  if (listTable) {
+    listTable.parentElement.removeChild(listTable);
+  }
+  var breadcrumb = document.querySelector('.stage .breadcrumb');
+  if (breadcrumb) {
+    breadcrumb.parentElement.removeChild(breadcrumb);
+  }
+  breadcrumbStack = [];
+  var selectedLabel = document.querySelector(".rows-selected-label");
+  if (selectedLabel) {
+    selectedLabel.parentElement.removeChild(selectedLabel);
+  }
+  document.querySelector('.stage-stack').removeEventListener('click', listStageStackClickHandler, false);
+  vizcanvas.classList.remove("hide");
+}
+
+function getAllRows() {
+  return Array.slice(document.querySelectorAll('.body-table tr'));
+}
+
+function getSelectedRows() {
+  // returns selected rows as an Array
+  return getAllRows().filter(function (item) {
+    return item.querySelector('.selected-row:checked');
+  });
+}
+
+// Event handlers
+
+function setUserSetting(row, pref) {
+  var site = row.dataset.name;
+
+  // change setting
+  userSettings[site] = pref;
+
+  // send change through to add-on
+  global.self.port.emit('updateBlocklist', site, pref === 'block');
+
+  // modify row
+  row.dataset.pref = pref;
+
+  // Add sort order to preference column
+  row.querySelector('.preferences').dataset.sortKey = pref;
+
+  // uncheck the row
+  row.querySelector('[type=checkbox]').checked = false;
+  row.classList.remove("checked");
+}
+
+
+// selectAllRows should only select VISIBLE rows
+function selectAllRows(flag) {
+  var i;
+  // apply flag to ALL rows first
+  var rows = document.querySelectorAll(".body-table tr");
+  for (i = 0; i < rows.length; i++) {
+    rows[i].querySelector(".selected-row").checked = flag;
+    highlightRow(rows[i], flag);
+  }
+  // and then exclude all the hidden rows
+  if (document.querySelector(".hide-hidden-rows")) {
+    var hiddenRows = document.querySelectorAll(".list-table .body-table tr[data-pref=hide]");
+    for (i = 0; i < hiddenRows.length; i++) {
+      hiddenRows[i].querySelector(".selected-row").checked = false; // makes sure the hidden rows are always unchecked
+      highlightRow(hiddenRows[i], false);
+    }
+  }
+  togglePrefButtons();
+}
+
+function setPreferences(pref) {
+  getSelectedRows().forEach(function (row) {
+    setUserSetting(row, pref);
+  });
+  document.querySelector('.selected-header').checked = false;
+  updateRowSelectedLabel();
+  togglePrefButtons();
+  toggleShowHideHiddenButton();
+}
+
+function toggleHiddenSites(target) {
+  if (target.dataset.state === 'shown') {
+    target.dataset.state = 'hidden';
+    target.textContent = 'Show Hidden';
+    document.querySelector('.stage-stack').classList.add('hide-hidden-rows');
+  } else {
+    target.dataset.state = 'shown';
+    target.textContent = 'Hide Hidden';
+    document.querySelector('.stage-stack').classList.remove('hide-hidden-rows');
+  }
+}
+
+var listStageStackClickHandler = function (event) {
+  var target = event.target;
+  if (target.mozMatchesSelector('label[for=block-pref], label[for=block-pref] *')) {
+    confirmBlockSitesDialog(function (confirmed) {
+      if (confirmed) {
+        setPreferences('block');
+      }
+    });
+  } else if (target.mozMatchesSelector('label[for=hide-pref], label[for=hide-pref] *')) {
+    if (doNotShowDialog(dialogNames.hideSites)) {
+      setPreferences('hide');
+    } else {
+      confirmHideSitesDialog(function (confirmed) {
+        if (confirmed) {
+          setPreferences('hide');
+        }
+      });
+    }
+  } else if (target.mozMatchesSelector('label[for=watch-pref], label[for=watch-pref] *')) {
+    setPreferences('watch');
+  } else if (target.mozMatchesSelector('label[for=no-pref], label[for=no-pref] *')) {
+    setPreferences('');
+  } else if (target.mozMatchesSelector('.toggle-hidden a')) {
+    toggleHiddenSites(target);
+  }
+};
+
+// Install handlers
+function initializeHandlers() {
+  try {
+    document.querySelector('.selected-header').addEventListener('change', function (event) {
+      selectAllRows(event.target.checked);
+    }, false);
+
+    document.querySelector('.list-footer').querySelector(".legend-toggle").addEventListener("click", function (event) {
+      toggleLegendSection(event.target, document.querySelector('.list-footer'));
+    });
+
+    document.querySelector('.stage-stack').addEventListener('click', listStageStackClickHandler, false);
+
+    // Add handler for rows
+    document.querySelector('.list-table').addEventListener('click', function (event) {
+      var url = event.target.parentNode.dataset.sortKey;
+      var node = event.target;
+      if (node.mozMatchesSelector('td:first-child [type=checkbox]')) {
+        while (node.mozMatchesSelector('.node *')) {
+          node = node.parentElement;
+        }
+        highlightRow(node, node.querySelector("[type=checkbox]").checked);
+        togglePrefButtons();
+      } else if (node.mozMatchesSelector('.update-table') && url) {
+        showFilteredTable(url);
+      }
+    }, false);
+
+    // Add handler to refresh rows 
+    var refreshRow = document.querySelector("#refresh-data-row");
+    refreshRow.addEventListener('click', function onClick() {
+      var wereSelected, selected;
+      refreshRow.classList.remove('show');
+      // update the table
+      // what were selected should stay selected after the table has been updated
+      wereSelected = getSelectedRows().map(function (row) {
+        return row.dataset.name;
+      });
+      showFilteredTable(lastFilter);
+      selected = getAllRows().filter(function (row) {
+        return wereSelected.indexOf(row.dataset.name) > -1;
+      })
+        .map(function (rowToSelect) {
+          rowToSelect.querySelector("[type=checkbox]").checked = true;
+          highlightRow(rowToSelect, true);
+          return;
+        });
+    }, false);
+
+    // Add handler to deselect rows
+    document.querySelector('.deselect').addEventListener('click', function (event) {
+      resetSelectedRows();
+    }, false);
+
+    // Set sort handlers. nth-child(n+2) skips the checkbox column
+    var table = document.querySelector(".list-table");
+    var headers = Array.prototype.slice.call(table.querySelectorAll('th:nth-child(n+2)'));
+    headers.forEach(function (th, idx) {
+      // idx+1 gives the actual column (skipping the checkbox the other way)
+      th.addEventListener('click', sortTableOnColumn(table, idx + 1), false);
+    });
+  } catch (e) {
+    console.log('Error: %o', e);
+  }
+}
+
+function highlightRow(node, rowChecked) {
+  if (rowChecked) {
+    node.classList.add("checked");
+  } else {
+    node.classList.remove("checked");
+  }
+  updateRowSelectedLabel();
+}
+
+function togglePrefButtons() {
+  var numChecked = document.querySelectorAll(".list-table .body-table tr input[type=checkbox]:checked").length;
+  var toggleOn = numChecked > 0;
+  var classToAdd = toggleOn ? "active" : "disabled";
+  var classToRemove = toggleOn ? "disabled" : "active";
+  // toggle on class
+  toArray(document.querySelectorAll("input[name=pref-options] + label")).forEach(function (option) {
+    option.classList.add(classToAdd);
+  });
+  // toggle off class
+  toArray(document.querySelectorAll("input[name=pref-options] + label")).forEach(function (option) {
+    option.classList.remove(classToRemove);
+  });
+}
+
+function toggleShowHideHiddenButton() {
+  if (document.querySelectorAll("[data-pref='hide']").length > 0) {
+    document.querySelector(".toggle-hidden").classList.remove("disabled");
+  } else {
+    document.querySelector(".toggle-hidden").classList.add("disabled");
+  }
+}
+
+})(visualizations, this);
diff --git a/resources/lightbeam/data/map.svg b/resources/lightbeam/data/map.svg
new file mode 100644
index 0000000..3683dce
--- /dev/null
+++ b/resources/lightbeam/data/map.svg
@@ -0,0 +1,12 @@
+<svg class="mapcanvas" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 400" width="100%" height="100%"><style>
+	.mapcanvas path{
+    fill: #E0E0E0;
+    stroke:#fff;
+    stroke-width:0.5;
+}
+
+.highlight-country{
+    fill: #0CA0E2 !important;
+}
+
+</style><path id="ae" d="M604.196,161.643L604.71,161.514L604.71,162.286L606.898,161.9L609.087,161.9L610.759,162.029L612.562,160.227L614.62,158.425L616.294,156.752L616.812,157.65200000000002L617.197,159.841L615.78,159.841L615.5219999999999,161.643L616.039,162.029L614.88,162.54399999999998L614.751,163.57299999999998L613.978,164.73199999999997L613.978,165.76199999999997L613.4639999999999,166.40599999999998L605.3539999999999,164.98999999999998L604.323,162.28599999999997L604.4499999999999,162 [...]
\ No newline at end of file
diff --git a/resources/lightbeam/data/parseuri.js b/resources/lightbeam/data/parseuri.js
new file mode 100644
index 0000000..f04257f
--- /dev/null
+++ b/resources/lightbeam/data/parseuri.js
@@ -0,0 +1,32 @@
+// parseUri 1.2.2
+// (c) Steven Levithan <stevenlevithan.com>
+// MIT License
+
+function parseUri(str) {
+  var o = parseUri.options,
+    m = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
+    uri = {},
+    i = 14;
+
+  while (i--) uri[o.key[i]] = m[i] || "";
+
+  uri[o.q.name] = {};
+  uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
+    if ($1) uri[o.q.name][$1] = $2;
+  });
+
+  return uri;
+}
+
+parseUri.options = {
+  strictMode: false,
+  key: ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "relative", "path", "directory", "file", "query", "anchor"],
+  q: {
+    name: "queryKey",
+    parser: /(?:^|&)([^&=]*)=?([^&]*)/g
+  },
+  parser: {
+    strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
+    loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
+  }
+};
diff --git a/resources/lightbeam/data/picoModal/LICENSE.md b/resources/lightbeam/data/picoModal/LICENSE.md
new file mode 100644
index 0000000..abd662b
--- /dev/null
+++ b/resources/lightbeam/data/picoModal/LICENSE.md
@@ -0,0 +1,23 @@
+The MIT License (MIT)
+---------------------
+
+Copyright (c) 2012 James Frasca
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/resources/lightbeam/data/picoModal/picoModal-1.0.0.min.js b/resources/lightbeam/data/picoModal/picoModal-1.0.0.min.js
new file mode 100644
index 0000000..1e625fd
--- /dev/null
+++ b/resources/lightbeam/data/picoModal/picoModal-1.0.0.min.js
@@ -0,0 +1 @@
+window.picoModal=function(a,b){"use strict";var c=function(){var b=[];return{w:function(a){b.push(a)},t:function(){for(var c=0;c<b.length;c++)a.setTimeout(b[c],1)}}},d=function(a){var c=b.createElement("div");(a||b.body).appendChild(c);var e={e:c,c:function(){return d(c)},s:function(a){a=a||{},"undefined"!=typeof a.opacity&&(a.filter="alpha(opacity="+100*a.opacity+")");for(var b in a)a.hasOwnProperty(b)&&(c.style[b]=a[b]);return e},z:function(a){return c.className+=a,e},h:function(a){ret [...]
\ No newline at end of file
diff --git a/resources/lightbeam/data/style.css b/resources/lightbeam/data/style.css
new file mode 100644
index 0000000..4136477
--- /dev/null
+++ b/resources/lightbeam/data/style.css
@@ -0,0 +1,1265 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/*
+  Visualization colour scheme:
+  trackers: #E73547
+  visited: #6CB4F9
+  connections: #434242
+
+  background: #2E2B2B
+  text: #EAEAEA
+  dropdown buttons: #363636
+  sidebar "link" buttons: #939393
+*/
+
+
+/*  New colour scheme:
+    sidebar, info panel
+        background:#404850
+*/
+
+html, body{
+    margin: 0;
+    padding: 0;
+    height: 100%;
+    background-color: #000;
+    color: #EAEAEA;
+    overflow: hidden;
+    font-size: 12px;
+}
+
+body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,aside,section,hgroup,footer,nav,figure,header,article,input,textarea,p,blockquote,th,td,select {
+    margin: 0;
+    padding: 0;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box;
+    font-family: "Open Sans";
+}
+
+a{
+    color: #73A4B8;
+    text-decoration: none;
+}
+
+a:hover{
+    color: #6FC3E5;
+}
+
+.large-header{
+    color: #73A4B8;
+    font-size: 18px;
+}
+
+.all-cap-header{
+    font-size: 10px;
+    text-transform: uppercase;
+    font-weight: bold;
+}
+
+.blue-text {
+    color: #73A4B8;
+}
+
+.grey-text{
+    color: #3E454D;
+}
+
+.main{
+    position: relative;
+    height: 100%;
+}
+
+.controls{
+    width: 170px;
+    padding: 15px 15px;
+    float: left;
+    z-index: 5;
+    height: 100%;
+    overflow-x: hidden;
+    overflow-y: auto;
+    background: #404850;
+}
+
+.controls .btn{
+    width:100%;
+}
+
+.controls header{
+    height: 35px;
+    margin-bottom: 20px;
+}
+
+.main header:hover{
+    cursor: pointer;
+}
+
+.logo{
+    width: 150px;
+    margin-left: -6px;
+}
+
+.hidden{
+    display: none !important;
+}
+
+.controls .section-header{
+    display: block;
+    margin: 15px 0 5px 5px;
+}
+
+/* Dialog/Popup Styling ================================ */
+
+.pico-content{
+    color: #000;
+    border-radius: 3px;
+    box-shadow: 5px 5px #555;
+}
+
+.pico-content .dialog-title{
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    text-transform: uppercase;
+    line-height: 35px;
+    font-weight: bold;
+    letter-spacing: 1px;
+    color: #fff;
+    background: #4CC7E6;
+    border-radius: 3px 3px 0 0;
+    padding-left: 20px;
+}
+
+.pico-content .dialog-content{
+    overflow: hidden;
+    margin-top: 30px;
+}
+
+.pico-content p{
+    margin-top: 15px;
+}
+
+.pico-content .dialog-sign{
+    float: left;
+    width: 20%;
+}
+
+.pico-content .dialog-sign img{
+    margin-top: 10px;
+    width: 100%;
+    height: 100%;
+}
+
+.pico-content .dialog-message{
+    float: left;
+    width: calc( 80% - 20px );
+    margin-left: 20px;
+}
+
+.pico-content .dialog-controls{
+    clear: both;
+}
+
+.pico-content .dialog-dns{
+    margin: 20px 0;
+}
+
+.pico-content .dialog-btns{
+    float: right;
+}
+
+.pico-content .dialog-ok,
+.pico-content .dialog-cancel{
+    display: inline-block;
+    background: #4CC7E6;
+    color: #fff;
+    font-size: 10px;
+    font-weight: bold;
+    border-radius: 3px;
+    padding: 5px 25px;
+    margin: 10px 0 0 10px;
+    cursor: pointer;
+}
+
+.pico-content .dialog-ok:focus,
+.pico-content .dialog-cancel:focus{
+    outline: 1px dotted grey;
+}
+
+.pico-content .privacy-policy{
+    height: 150px;
+    overflow-y: auto;
+    border: 1px dashed #ccc;
+    padding: 10px 0;
+    margin: 0 5px;
+}
+
+.pico-content .privacy-policy.collapsed{
+    display: none;
+}
+
+.privacy-policy ul{
+    margin-left: 15px;
+    list-style-type: circle !important;
+}
+
+/* Button Styling ================================ */
+
+.btn{
+    margin-bottom: 5px;
+    background: #171E25;
+    letter-spacing: 1px;
+    border-radius: 5px;
+}
+
+.btn a:hover,
+.btn_group .dropdown_options a:hover{
+    background: #6FC3E5;
+    cursor: pointer;
+}
+
+.btn_group .dropdown_options a:hover{
+    margin: 0 2px;
+}
+
+.btn_group a.selected_time,
+.btn_group a.selected_visualization{
+    color: #EAEAEA;
+}
+
+.btn a{
+    border-radius: 3px;
+}
+
+.btn_group div.dropdown_options a,
+.btn a{
+    margin: 0;
+    display: block;
+    padding-left: 10px;
+    background: #171E25;
+    line-height: 28px;
+    text-transform: capitalize;
+    color: #fff;
+}
+
+.btn_group .dropdown_options a{
+    border-radius: 0;
+    border-top: 2px solid #12181B;
+}
+
+.btn_group .dropdown_options a[data-selected]{
+    background: #73A4B8;
+    margin: 0 2px;
+    border-top: 2px solid #6FC3E5;
+}
+
+.btn_group .dropdown_options a:first-child[data-selected]{
+    box-shadow: 0 -2px #6FC3E5;
+}
+
+.btn_group .dropdown_options a:last-child[data-selected]{
+    box-shadow: none;
+    border-top: 2px solid #6FC3E5;
+}
+
+
+.btn_group .dropdown_options a:first-child{
+    border-radius: 3px 3px 0 0;
+    border-top: none;
+}
+
+.btn_group .dropdown_options a:last-child{
+    border-radius: 0 0 3px 3px;
+    box-shadow: 0 5px #12181B;
+}
+
+.btn_group img,
+.btn img{
+    width: 15px;
+    height: 15px;
+    margin-right: 15px;
+    position: relative;
+    top: 3px;
+}
+
+.controls .links{
+    text-align: center;
+    line-height: 30px;
+    margin-top: 15px;
+}
+
+.controls .links img{
+    width: 15px;
+    height: 15px;
+}
+
+
+.toggle-btn input{
+    display: none;
+}
+
+.toggle-btn .toggle-btn-innner{
+    display: inline-block;
+    background: #5F6771;
+    border-radius: 5px;
+    width: 60px;
+    height: 28px;
+    padding: 3px;
+    cursor: pointer;
+}
+
+.toggle-btn .toggle-btn-innner > * {
+    display: inline-block;
+}
+
+.toggle-btn .toggle-btn-innner .switch{
+    float: left;
+    width: 45%;
+    height: 100%;
+    background: #303539;
+    border-radius: 5px;
+}
+
+.toggle-btn .toggle-btn-innner .on-off-text{
+    float: right;
+    line-height: 20px;
+    font-size: 10px;
+    font-weight: bold;
+    margin: 0 5px;
+}
+
+.toggle-btn input + .toggle-btn-innner.checked{
+    background: #25292D;
+}
+
+.toggle-btn input + .toggle-btn-innner .switch.checked{
+    float: right;
+    background: #4CC7E6;
+}
+
+.toggle-btn input + .toggle-btn-innner .on-off-text.checked{
+    float: left;
+}
+
+/* Button Styling ends ================================ */
+
+
+#content{
+    flex:1;
+    position: relative;
+    z-index: 0;
+    border-top: 1px solid #555555;
+    height: 100%;
+}
+
+.content-flex{
+    display:flex;
+    flex-direction:column;
+    height:100%;
+    width:calc(100% - 170px);
+}
+
+.top-bar{
+    background-color:#fff;
+    flex:none;
+    padding: 20px 20px 5px 20px;
+    text-transform: uppercase;
+}
+
+.stats-section{
+    float: left;
+    color: #3E454D;
+}
+
+.stats-section section{
+    float: left;
+    margin-right: 20px;
+}
+
+.sharing-section{
+    float: right;
+    text-align: center;
+}
+
+.share-btn label{
+    position: relative;
+    top: 10px;
+}
+
+.sharing-section .label{
+    color: #3E454D;
+    margin-right: 5px;
+}
+
+.sharing-section div{
+    display: inline-block;
+}
+
+
+.info{
+    width: 8px;
+    height: 100%;
+    float: right;
+    border-left: none;
+    transition: width 0.5s ease;
+    overflow-x: hidden;
+    overflow-y: auto;
+    background: #404850;
+}
+
+.info-panel-controls{
+    display: block;
+    float: right;
+    z-index: 1000;
+    margin: 10px 0 0 0;
+}
+
+.info-panel-controls ul{
+    list-style-type: none;
+}
+
+.info-panel-controls ul li{
+    width: 28px;
+    height: 28px;
+    position: relative;
+    bottom: 10px;
+    margin-bottom: 5px;
+    padding: 18px;
+    background: #20272E;
+    border-radius: 2px 0 0 2px;
+    cursor: pointer;
+    font-size: 15px;
+}
+
+.info-panel-controls ul li.active{
+    background: #404850;
+}
+
+.info-panel-controls img{
+    width: 15px;
+    height: 15px;
+}
+
+.info-panel-controls img,
+.info-panel-controls .icon-chevron-right{
+    position: relative;
+    right: 8px;
+    bottom: 8px;
+}
+
+.toggle-site-profile.disabled{
+    cursor: default;
+    opacity: 0.2;
+}
+
+.info .holder{
+    display:flex;
+    flex-direction:column;
+    width: 300px;
+    height:100%;
+    padding: 17px 23px 8px 18px;
+}
+
+
+
+/* Help Sections */
+
+.help-content svg{
+    width: 15px;
+    height: 15px;
+    position: relative;
+    top: 2px;
+}
+
+.info .holder > div .large-header{
+    margin-bottom: 10px;
+}
+
+.info .holder > div .large-header img{
+    width: 16px;
+    height: 16px;
+}
+
+.grey-label{
+    display: inline-block;
+    margin-right: 5px;
+    color: #777;
+    letter-spacing: 1px;
+    font-weight: bold;
+}
+
+.help-content .blue-text.all-cap-header,
+.about-content .blue-text.all-cap-header{
+    margin-bottom: 10px;
+    font-weight: bold;
+}
+
+.info .holder section{
+    padding: 15px 0;
+}
+
+.info .holder section:not(:last-of-type){
+    border-bottom: 1px solid #303840;
+}
+
+.info .holder section p{
+    margin: 10px 0;
+}
+
+.info .holder section ul{
+    list-style-type: none;
+}
+
+.info .holder section ul.bullet-form{
+    list-style-type: circle;
+}
+
+.info .holder section ul.bullet-form li{
+    list-style-type: disc;
+    margin-left: 15px;
+}
+
+.privacy-policy header{
+    margin-top: 15px;
+}
+
+.info .holder section img{
+    width: 15px;
+    height: 15px;
+    margin-right: 5px;
+}
+
+.feature-name{
+    display: inline-block;
+    width: 120px;
+    font-weight: bold;
+    text-transform: capitalize;
+}
+
+/* Info Panel(Site Profile) Sections */
+
+.map-section{
+    margin: 10px 0 10px 0;
+}
+
+
+.world-map, .connections-list ul{
+    background-color: #303539;
+    border-radius:5px;
+}
+
+.connections-list{
+    flex:1;
+    display:flex;
+    flex-direction:column;
+}
+
+.pref-tag img{
+    width: 15px;
+    height: 15px;
+    position: relative;
+    top: 3px;
+}
+
+/* End Map Styles */
+
+.info .holder .favicon{
+    width: 20px;
+    height: 20px;
+    margin-right: 10px;
+}
+
+.showinfo .info{
+    width: 300px;
+}
+
+.info .filters{
+    margin-bottom: 1em;
+}
+
+.info .filters h2{
+    white-space: nowrap;
+    font-size: 10px;
+    font-color: #CCC;
+    width: 100%;
+}
+
+.info .closed p{
+    display: none;
+}
+
+.info .disclosure{
+    cursor: pointer;
+}
+.info .disclosure:before{
+    content: "▼";
+    padding-right: 1em;
+}
+
+.info .closed .disclosure:before{
+    content: "▶";
+}
+
+.sorted:after, .reverse-sorted:after{
+    padding:0px 5px;
+    content: '▾';
+    display: inline-block;
+    position: relative;
+    -moz-transition:all 0.3s linear;
+}
+
+.sorted:after{
+    transform:rotate(0deg);
+}
+
+.reverse-sorted:after{
+    transform:rotate(180deg);
+}
+
+.connections-list ul{
+    line-height: 15px;
+    list-style-type: none;
+    min-height: 150px;
+    overflow-y: auto;
+    flex:1;
+}
+
+.connections-list ul li{
+    padding:2px 2px 2px 10px;
+    cursor: pointer;
+}
+
+.connections-list ul li:hover{
+    background-color: #6FC3E5;
+}
+
+.connections-list ul li[data-selected]{
+    background-color: #73A4B8;
+}
+
+.connections-list ul li.disabled{
+    color: #555;
+    pointer-events: none;
+}
+
+.stage-stack{
+    padding: 20px 0 20px 40px;
+    flex-direction: column;
+    display: flex;
+    position: relative;
+    height: 100%;
+    overflow: hidden;
+}
+
+.stage{
+    flex: 1;
+    /*margin: 20px 0px;*/
+}
+.showinfo .stage-stack{
+    /*margin-right: 300px;*/
+}
+
+
+
+/* SVG STYLES */
+
+text {
+    fill: #fff;
+    font-size: 9px;
+    font-variant: small-caps;
+    text-anchor: middle;
+}
+
+#tooltip{
+    display: none;
+    position: absolute;
+    background-color: #FFF;
+    color: #010203;
+    padding: 5px 10px;
+    box-shadow: 0px 2px #4CC7E6;
+    border-radius: 5px;
+}
+#tooltip:after{
+    content: '';
+    width: 0;
+    height: 0;
+    border: 10px solid transparent;
+    border-top: 10px solid #FFF;
+    position: absolute;
+    top: 100%;
+    left: 50%;
+    margin-left: -10px;
+}
+
+/* CLOCK Visualization */
+
+.source,
+.source-sites,
+.target,
+.target-sites{
+    fill: #FFF;
+    stroke: none;
+}
+
+.source.highlighted,
+.target.highlighted{
+    opacity: 1;
+}
+
+
+.source text, .target text{
+    font-size: 6px;
+    text-anchor: right;
+    stroke: #FFF;
+    visibility: hidden;
+}
+
+.times-label{
+    font-size: 15px;
+}
+
+.times-am-pm-label{
+    fill: #3E454D;
+    font-size: 10px;
+}
+
+.tracker rect{
+    visibility: hidden;
+}
+
+.tracker:hover text, .tracker:hover rect{
+    visibility: visible;
+}
+
+#timerhand{
+    fill: rgba(76, 199, 230, .6);
+    stroke: rgba(76, 199, 230, .6);
+    strokewidth: 3px;
+}
+
+.greyed-out{
+    opacity: 0.2;
+}
+
+/*.clicked-node.node circle:last-child{
+    stroke-width: 2;
+    fill: #fff;
+}
+
+.clicked-node.node.source circle:last-child{
+    stroke: #128764;
+}
+
+.clicked-node.node.target circle:last-child{
+    stroke: #F1C40F;
+}
+
+.colluded-source.node circle:last-child, .colluded-target.node circle:last-child{
+    stroke: #fff;
+    stroke-width: 1;
+}*/
+
+/* Graph Visualization */
+
+.filter-display{
+    display: inline-block;
+    position: absolute;
+    background: rgba(0,0,0,0.9);
+}
+
+.filter-display header{
+    text-transform: capitalize;
+    font-size: 30px;
+}
+
+.visitedSites,
+.unvisitedSites,
+.selectedSites,
+.colludedSites{
+    fill: #FFF;
+}
+
+.connectionLine{
+    stroke: #FFF;
+    stroke-width: 2;
+}
+
+.watchedSites{
+    fill: #6FC3E5 !important;
+}
+
+.watch-text{
+    color: #6FC3E5;
+}
+
+.hide-text{
+    color: #FE7E00;
+}
+
+.blockedSites{
+    fill: #E02A61 !important;
+}
+
+.block-text{
+    color: #E02A61;
+}
+
+.cookie-text{
+    color: #6C0AAA;
+}
+
+.edge{
+    stroke: #FFF;
+    opacity: 0.3;
+}
+
+.visitedYes,
+.visitedNo,
+.visitedBoth{
+    fill: #FFFFFF;
+    opacity: 0.3;
+}
+
+.visitedYes.highlighted,
+.visitedNo.highlighted,
+.visitedBoth.highlighted,
+.edge.highlighted{
+    opacity: 1;
+}
+
+.cookieYes.coloured,
+.cookieBoth.coloured,
+.cookies{
+    stroke: #6C0AAA;
+}
+
+/* List Visualization */
+.hide {
+    display: none;
+    width: 0;
+    height: 0;
+}
+
+.breadcrumb{
+    display: inline-block;
+    overflow: hidden;
+    margin-bottom: 15px;
+}
+
+.breadcrumb .breadcrumb-chunk{
+    float: left;
+    cursor: pointer;
+    color: #555;
+}
+
+.breadcrumb .breadcrumb-chunk.no-click{
+    cursor: default;
+    color: #4CC7E6;
+}
+
+.breadcrumb .arrow-left {
+    float: left;
+    width: 0;
+    height: 0;
+    border-top: 5px solid transparent;
+    border-bottom: 5px solid transparent;
+    border-right:10px solid #303539;
+    margin: 7px 10px 0 10px;
+}
+
+.rows-selected-label{
+    float: right;
+    margin-right: 10px;
+    text-align: right;
+}
+
+.rows-selected-label div{
+    display: inline-block;
+}
+
+.rows-selected-label .deselect {
+    color: white;
+    font-weight: bold;
+    margin-top: 5px;
+}
+
+.rows-selected-label .deselect:hover {
+    cursor: pointer;
+}
+
+.num-selected, .num-total{
+    font-weight: bold;
+}
+
+.list-table{
+    border-collapse: collapse;
+    height: 100%;
+    margin-right: 10px;
+}
+
+.list-table table{
+    width: 100%;
+    border-collapse: collapse;
+    border-spacing: 0;
+}
+
+.header-table{
+    background-color:#404040;
+    margin-right: 20px;
+}
+
+.body-table{
+    height: calc(100% - 40px);
+    overflow-y: scroll;
+}
+
+
+.list-table th,
+.list-table td{
+    border-bottom: 1px solid #555;
+    line-height: 35px;
+    margin: 0;
+}
+
+.list-table th{
+    text-align: left;
+    cursor:pointer;
+    -moz-user-select: -moz-none !important;
+}
+
+.list-table [role]{
+    outline: 0;
+}
+
+.list-table tr[site-url]{
+    cursor: pointer;
+}
+
+.list-table tr.selected-connected-row{
+    background-color: #73A4B8;
+}
+
+[data-pref=watch]{
+    color: teal;
+}
+
+.preferences{
+    background-size: 16px;
+    background-repeat: no-repeat;
+    background-position: center left;
+}
+
+[data-pref=watch] .preferences{
+    background-image: url(icons/lightbeam_icon_watch.png);
+}
+
+[data-pref=block]{
+    color: red;
+}
+
+[data-pref=block] .preferences{
+    background-image: url(icons/lightbeam_icon_block.png);
+}
+
+[data-pref=hide]{
+    color: orange;
+}
+
+[data-pref=hide] .preferences{
+    background-image: url(icons/lightbeam_icon_hide.png);
+}
+
+.hide-hidden-rows [data-pref=hide]{
+    display: none;
+}
+
+.list-table tbody tr:hover,
+.list-table tbody tr.checked{
+    background: rgba(255,255,255,1);
+    color: #000;
+}
+
+.list-table .visited-row td{
+    color: #6178FA;
+}
+
+.list-table .third-row td{
+    color: #FF617B;
+}
+
+.header-table th:nth-child(1), .body-table td:nth-child(1){
+    padding-left:10px;
+    width: 35px;
+}
+
+.header-table th:nth-child(2), .body-table td:nth-child(2){
+    width: 6em;
+}
+
+.header-table th:nth-child(3), .body-table td:nth-child(3){
+    width: 5em;
+}
+
+.body-table td:nth-child(4) img{
+    width: 16px;
+    margin-left: 4px;
+    margin-right: 4px;
+}
+
+.header-table th:nth-child(5), .body-table td:nth-child(5){
+    width: 10em;
+}
+
+.header-table th:nth-child(6), .body-table td:nth-child(6){
+    width: 10em;
+}
+
+.header-table th:nth-child(7){
+    width: 130px;
+}
+.body-table td:nth-child(7){
+    width: 115px;
+    text-align: right;
+    padding-right: 15px;
+}
+
+
+
+/* Legend for Graph and Site Preferences for List*/
+
+.graph-footer,
+.list-footer{
+    margin-right: 8px;
+    flex: none;
+}
+
+.list-footer{
+    margin-top: 50px;
+}
+
+.legend-header{
+    overflow: hidden;
+    border-bottom: 1px solid #fff;
+    padding-bottom: 5px;
+}
+
+.legend-header div:first-of-type{
+    float: left;
+}
+
+.legend-header div:last-of-type{
+    width: 170px;
+    float: right;
+}
+
+.legend-header .legend-toggle{
+    float:right;
+}
+
+.legend-header header{
+    display: inline-block;
+    margin-bottom: 3px;
+}
+
+.legend-header .legend-toggle{
+    display: inline-block;
+    font-size: 10px;
+    cursor: pointer;
+}
+
+.legend-controls{
+    clear: both;
+    padding: 10px 0;
+}
+
+.legend-controls .legend-label{
+    margin-bottom: 15px;
+    font-size: 10px;
+    letter-spacing: 1px;
+    border-radius: 5px;
+    width: 170px;
+}
+
+.legend-controls .column{
+    float: left;
+}
+
+.legend-controls .legend-canvas-small,
+.legend-controls .legend-canvas-large{
+    height: 18px;
+    position: relative;
+    top: 6px;
+    margin-right: 5px;
+}
+
+.legend-controls .column .btn img{
+    position: relative;
+    top: 4px;
+    margin-right: 5px;
+}
+
+.legend-controls .legend-canvas-small{
+    width: 17px;
+}
+
+.legend-controls .legend-canvas-large{
+    width: 34px;
+}
+
+.legend-controls .btn a{
+    font-size: 10px;
+}
+
+.graph-footer .btn {
+    width: 170px;
+    margin-right: 30px;
+    margin-bottom: 8px;
+}
+
+.graph-footer .btn a{
+    border-top: 2px solid #171E25;
+    color: #555;
+}
+
+.graph-footer .btn a:hover{
+    border-top: 2px solid #fff !important;
+    opacity: 1;
+    color: #fff;
+}
+
+.graph-footer .btn.active a{
+    border-top: 2px solid #6FC3E5;
+    background: #73A4B8;
+    color: #fff;
+}
+
+.graph-footer .btn_group{
+    float: right;
+    width: 150px;
+}
+
+.graph-footer .btn_group .all-cap-header{
+    margin-right: 10px;
+}
+
+.graph-footer .btn_group .dropdown_options{
+    width: 170px;
+}
+
+.list-footer .align-left,
+.list-footer .align-right{
+    display: inline-block;
+    overflow: hidden;
+}
+
+.align-left{
+    float: left;
+}
+
+.align-right{
+    float: right;
+}
+
+.list-footer .align-left .btn{
+    float: left;
+    width: 140px;
+    margin-right: 20px;
+}
+
+.list-footer .align-right .btn{
+    float: left;
+    margin-left: 20px;
+}
+
+.list-footer .align-right .btn a{
+    padding: 0 15px;
+}
+
+.list-footer .btn.disabled{
+    opacity: 0.15;
+    pointer-events: none;
+}
+
+.list-footer .btn{
+    opacity: 1;
+    pointer-events: auto;
+}
+
+
+.list-footer .btn a,
+.list-footer input[name=pref-options] + label{
+    background: #888;
+    color: #fff;
+}
+
+.list-footer .btn a:hover,
+.list-footer input[name=pref-options] + label:hover{
+    background: #6FC3E5;
+    cursor: pointer;
+}
+
+.list-footer label[for=block-pref]{
+    box-shadow: 0 2px #E02A61;
+}
+
+.list-footer label[for=hide-pref],
+.list-footer .btn.toggle-hidden{
+    box-shadow: 0 2px #FE7E00;
+}
+
+.list-footer label[for=watch-pref]{
+    box-shadow: 0 2px #6FC3E5;
+}
+
+.list-footer input[name=pref-options]{
+    display: none;
+}
+
+.list-footer input[name=pref-options] + label{
+    margin-right: 10px;
+    width: 140px;
+    line-height: 28px;
+}
+
+.list-footer input[name=pref-options] + label img{
+    width: 15px;
+    height: 15px;
+    margin: 0 5px;
+}
+
+/*.list-footer input[name=pref-options]:checked + label{
+    border: 1px solid red;
+}*/
+
+.list-footer input[name=pref-options] + label .radio-dot{
+    width: 15px;
+    height: 15px;
+    background-image: url(image/Lightbeam_radio_off.png);
+    background-repeat: no-repeat;
+    background-size: 15px 15px;
+    display: inline-block;
+    margin-left: 5px;
+    position: relative;
+    top: 3px;
+
+}
+
+.list-footer input[name=pref-options]:checked + label .radio-dot{
+    background-image: url(image/Lightbeam_radio_on.png);
+}
+
+tr.refresh {
+  text-align: center;
+  background-color: rgb(23, 30, 37);
+  visibility: collapse;
+}
+
+tr.refresh.show {
+  visibility: visible;
+}
+
+tr.refresh.show:hover {
+  background-color: rgb(115, 164, 184);
+  cursor: pointer;
+}
diff --git a/resources/lightbeam/data/svgdataset.js b/resources/lightbeam/data/svgdataset.js
new file mode 100644
index 0000000..f0ec8a9
--- /dev/null
+++ b/resources/lightbeam/data/svgdataset.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+(function (global) {
+
+function dataAttrToKey(attr) {
+  return attr.slice(5).split('-').map(function (part, index) {
+    if (index) {
+      return part[0].toUpperCase() + part.slice(1);
+    }
+    return part;
+  }).join('');
+}
+
+function dataKeyToAttr(key) {
+  return 'data-' + key.replace(/([A-Z])/, '-$1').toLowerCase();
+}
+
+function svgdataset(elem) {
+  // work around the fact that SVG elements don't have dataset attributes
+  var ds = function (key, value) {
+    if (value === undefined) {
+      // act as getter
+      value = elem.getAttribute(dataKeyToAttr(key));
+      try {
+        return JSON.parse(value);
+      } catch (e) {
+        return value;
+      }
+    } else {
+      var s = JSON.stringify(value);
+      elem.setAttribute(dataKeyToAttr(key), s);
+      return s;
+    }
+  };
+  // Create read-only shortcuts for convenience
+  Array.prototype.forEach.call(elem.attributes, function (attr) {
+    if (attr.name.startsWith('data-')) {
+      try {
+        ds[dataAttrToKey(attr.name)] = JSON.parse(attr.value);
+      } catch (e) {
+        ds[dataAttrToKey(attr.name)] = attr.value;
+      }
+    }
+  });
+  return ds;
+}
+
+global.svgdataset = svgdataset;
+
+})(this);
diff --git a/resources/lightbeam/data/tooltip.js b/resources/lightbeam/data/tooltip.js
new file mode 100644
index 0000000..feae81e
--- /dev/null
+++ b/resources/lightbeam/data/tooltip.js
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+// Manage hover-over tooltips (works differently in graph, list, clock views)
+(function (global) {
+
+var tooltipTimer;
+var tooltip;
+
+// for Clock view
+function showTooltip(event) {
+  if (!tooltip) {
+    tooltip = document.getElementById('tooltip');
+  }
+  tooltip.style.left = '-1000px';
+  tooltip.style.display = 'inline-block';
+  // console.error(event, event.target, event.target.dataset);
+  tooltip.textContent = event.target.getAttribute(["data-name"]);
+  var rect = event.target.querySelector(":last-child").getClientRects()[0];
+  var tooltipWidth = tooltip.offsetWidth;
+  tooltip.style.top = (rect.top - 40) + 'px';
+  tooltip.style.left = (rect.left + (rect.width / 2) - (tooltipWidth / 2)) + 'px';
+  setTooltipTimeout();
+  return false;
+}
+
+// for Graph view
+function d3ShowTooltip(node, idx) {
+  if (!tooltip) {
+    tooltip = document.getElementById('tooltip');
+  }
+  tooltip.style.left = '-1000px';
+  tooltip.style.display = 'inline-block';
+  // console.error(event, event.target, event.target.dataset);
+  tooltip.textContent = node.name;
+  var shapeNode = this.querySelector(".site") || this.querySelector("[data-name]"); // look for "site"(circle) node or "tracker(triangle)" node
+  var rect = shapeNode.getClientRects()[0];
+  var tooltipWidth = tooltip.offsetWidth;
+  tooltip.style.top = (rect.top - 40) + 'px';
+  tooltip.style.left = (rect.left + (rect.width / 2) - (tooltipWidth / 2)) + 'px';
+  return false;
+}
+
+// for List view
+function listShowTooltip(event) {
+  if (!tooltip) {
+    tooltip = document.getElementById('tooltip');
+  }
+  tooltip.style.left = '-1000px';
+  tooltip.style.display = 'inline-block';
+  // console.error(event, event.target, event.target.dataset);
+  tooltip.textContent = "go to " + event.target.parentElement.getAttribute(["data-sort-key"]) + "'s site list";
+  var rect = event.target.getClientRects()[0];
+  var tooltipWidth = tooltip.offsetWidth;
+  tooltip.style.top = (rect.top - 40) + 'px';
+  tooltip.style.left = (rect.left + (rect.width / 2) - (tooltipWidth / 2)) + 'px';
+  setTooltipTimeout();
+  return false;
+}
+
+
+function setTooltipTimeout() {
+  if (tooltipTimer) {
+    clearTimeout(tooltipTimer);
+  }
+  tooltipTimer = setTimeout(function () {
+    timeoutTooltip();
+  }, 2000);
+}
+
+function timeoutTooltip() {
+  tooltip.style.display = 'none';
+  tooltip.timer = null;
+}
+
+function hideTooltip() {
+  timeoutTooltip();
+  return false;
+}
+
+function add(node) {
+  node.addEventListener('mouseenter', showTooltip, false);
+  node.addEventListener('mouseleave', hideTooltip, false);
+}
+
+function remove(node) {
+  node.removeEventListener('mouseenter', showTooltip);
+  node.removeEventListener('mouseleave', hideTooltip);
+}
+
+
+global.tooltip = {
+  add: add,
+  remove: remove,
+  show: d3ShowTooltip,
+  hide: hideTooltip,
+  addTooltip: listShowTooltip
+};
+
+})(this);
diff --git a/resources/lightbeam/data/ui.js b/resources/lightbeam/data/ui.js
new file mode 100644
index 0000000..b8e6959
--- /dev/null
+++ b/resources/lightbeam/data/ui.js
@@ -0,0 +1,516 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+(function (global) {
+
+// In 1.0.9 and before, the contribute data pref was kept in localStorage. In
+// 1.0.10, all prefs are stored in the pref manager. If a previous pref exists,
+// send it back to the addon. This logic is ridiculous but it's what existed
+// prior to 1.0.9, so just keep it in.
+if (localStorage.userHasOptedIntoSharing === 'true') {
+  console.log("Restoring contribute data pref from localStorage");
+  let e = { "contributeData": true };
+  global.self.port.emit("prefChanged", e);
+  updateUIFromPrefs(e);
+  localStorage.clear();
+}
+
+// Bunch of utilities related to UI elements.
+const graphNodeRadius = {
+  "graph": 12
+};
+
+var g = global;
+global.graphNodeRadius = graphNodeRadius;
+
+/* Convert a NodeList to Array */
+global.toArray = function toArray(nl) {
+  return Array.prototype.slice.call(nl, 0);
+};
+
+/**************************************************
+ *   For accessibility:
+ *       if the current focused element is an anchor, press Enter will mimic mouse click on that element
+ */
+document.addEventListener("keypress", function (event) {
+  var focusedElm = document.activeElement;
+  if (event.keyCode == "13" && focusedElm.mozMatchesSelector("a") && !focusedElm.getAttribute("href")) {
+    focusedElm.click();
+  }
+});
+
+/* Lightbeam Logo Click handler ====================== */
+document.querySelector(".main header").addEventListener("click", function () {
+  location.reload();
+});
+
+
+/**************************************************
+ *   Buttons
+ */
+
+function dropdownGroup(btnGroup, callback) {
+  callback = callback || function () {};
+  var allOptions = btnGroup.querySelectorAll(".dropdown_options a");
+  toArray(allOptions).forEach(function (option) {
+    option.addEventListener("click", function (e) {
+      btnGroup.querySelector("[data-selected]").removeAttribute("data-selected");
+      e.target.setAttribute("data-selected", true);
+      callback(e.target.getAttribute("data-value"));
+    });
+  });
+}
+
+/* Bind click event listener to each of the btn_group memebers */
+var btnGroupArray = toArray(document.querySelectorAll(".btn_group"));
+btnGroupArray.forEach(function (btnGroup) {
+  dropdownGroup(btnGroup, function (val) {
+    val = val.toLowerCase();
+    switch (val) {
+    case 'graph':
+    case 'list':
+      switchVisualization(val);
+      break;
+    case 'recent':
+    case 'last10sites':
+    case 'daily':
+    case 'weekly':
+      aggregate.switchFilter(val);
+      document.querySelector(".filter-display header").textContent = btnGroup.querySelector("[data-selected]").textContent;
+      break;
+    default:
+      console.log("selected val=" + val);
+    }
+  });
+});
+
+
+/* Share Data Toggle */
+
+document.querySelector(".toggle-btn.share-btn").addEventListener("click",
+  function (event) {
+    var elmClicked = event.target;
+    if (elmClicked.mozMatchesSelector("input")) {
+      if (elmClicked.checked) {
+        confirmStartSharing(true, elmClicked);
+      } else {
+        confirmStopSharing(elmClicked);
+      }
+    }
+  });
+
+function confirmStartSharing(askForConfirmation, elmClicked) {
+  let callback = function (confirmed) {
+    if (confirmed) {
+      console.debug("Sharing confirmed!");
+      toggleBtnOnEffect(document.querySelector(".share-btn"));
+      global.self.port.emit("prefChanged", {
+        "contributeData": true
+      });
+    } else {
+      elmClicked.checked = false;
+    }
+  };
+  if (askForConfirmation) {
+    askForDataSharingConfirmationDialog(callback);
+  } else {
+    callback(true);
+  }
+
+}
+
+global.confirmStopSharing = function confirmStopSharing(elmClicked) {
+  stopSharingDialog(function (confirmed) {
+    if (confirmed) {
+      toggleBtnOffEffect(document.querySelector(".share-btn"));
+      global.self.port.emit("prefChanged", {
+        "contributeData": false
+      });
+    } else {
+      elmClicked.checked = true;
+    }
+  });
+};
+
+function toggleBtnOnEffect(toggleBtn) {
+  toggleBtn.querySelector(".toggle-btn-innner").classList.add("checked");
+  toggleBtn.querySelector(".switch").classList.add("checked");
+  toggleBtn.querySelector(".on-off-text").classList.add("checked");
+  toggleBtn.querySelector(".on-off-text").textContent = "ON";
+}
+
+function toggleBtnOffEffect(toggleBtn) {
+  toggleBtn.querySelector(".toggle-btn-innner").classList.remove("checked");
+  toggleBtn.querySelector(".switch").classList.remove("checked");
+  toggleBtn.querySelector(".on-off-text").classList.remove("checked");
+  toggleBtn.querySelector(".on-off-text").textContent = "OFF";
+}
+
+function downloadAsJson(data, defaultFilename) {
+  var file = new Blob([data], {
+    type: 'application/json'
+  });
+  var reader = new FileReader();
+  var a = document.createElement('a');
+  reader.onloadend = function () {
+    a.href = reader.result;
+    a.download = defaultFilename;
+    a.target = '_blank';
+    document.body.appendChild(a);
+    a.click();
+  };
+  reader.readAsDataURL(file);
+}
+
+
+document.querySelector(".download").addEventListener('click', function (evt) {
+  // console.log('received export data');
+  downloadAsJson([JSON.stringify(allConnections)], 'lightbeamData.json');
+  evt.preventDefault();
+  // window.open('data:application/json,' + exportFormat(allConnections));
+});
+
+
+
+document.querySelector('.reset-data').addEventListener('click', function () {
+  confirmResetDataDialog(function (confirmed) {
+    if (confirmed) {
+      // currentVisualization.emit('remove');
+      allConnections = [];
+      global.self.port.emit('reset');
+      aggregate.emit('reset');
+      // WARNING: this is a race condition. Since the event emitters are
+      // async, we were running into the situation where the page was reloaded
+      // before aggregate::resetData was finished, resulting in #506
+      //
+      // TODO: using a short timeout for now, would be better to use a Promise
+      setTimeout(500, function () { location.reload(); /* reload page */ });
+    }
+  });
+});
+
+global.getZoom = function getZoom(canvas) {
+  try {
+    var box = canvas.getAttribute('viewBox')
+      .split(/\s/)
+      .map(function (i) {
+        return parseInt(i, 10);
+      });
+    return {
+      x: box[0],
+      y: box[1],
+      w: box[2],
+      h: box[3]
+    };
+  } catch (e) {
+    console.log('error in getZoom, called with %o instead of an element');
+    console.log('Caller: %o', caller);
+    return null;
+  }
+};
+
+global.setZoom = function setZoom(box, canvas) {
+  // TODO: code cleanup if both cases use basically the same code
+  canvas.setAttribute('viewBox', [box.x, box.y, box.w, box.h].join(' '));
+};
+
+
+/* Scroll over visualization to zoom in/out ========================= */
+
+/* define viewBox limits
+ *  graph view default viewBox = " 0 0 750 750 "
+ *  map                        = " 0 0 2711.3 1196.7 "
+ */
+const graphZoomInLimit = {
+  w: 250,
+  h: 250
+};
+const graphZoomOutLimit = {
+  w: 4000,
+  h: 4000
+};
+const mapZoomInLimit = {
+  w: (2711.3 / 5),
+  h: (1196.7 / 5)
+};
+const mapZoomOutLimit = {
+  w: 2711.3,
+  h: 1196.7
+};
+const svgZoomingRatio = 1.1;
+
+document.querySelector(".stage").addEventListener("wheel", function (event) {
+  if (event.target.mozMatchesSelector(".vizcanvas, .vizcanvas *") && g.currentVisualization.name != "list") {
+    if (g.currentVisualization.name == "graph") {
+      zoomWithinLimit(event.deltaY, vizcanvas, graphZoomInLimit, graphZoomOutLimit);
+    }
+  }
+}, false);
+
+
+// check to see if the viewBox of the targeting svg is within the limit we define
+function checkWithinZoomLimit(targetSvg, zoomType, zoomLimit) {
+  var currentViewBox = getZoom(targetSvg);
+  if (zoomType == "in") {
+    var withinZoomInLimit = (currentViewBox.w > zoomLimit.w && currentViewBox.h > zoomLimit.h);
+    if (zoomLimit.x && zoomLimit.y) {
+      withinZoomInLimit =
+        withinZoomInLimit && (currentViewBox.x < zoomLimit.x && currentViewBox.y < zoomLimit.y);
+    }
+    return withinZoomInLimit;
+  } else {
+    var withinZoomOutLimit = (currentViewBox.w <= zoomLimit.w && currentViewBox.h <= zoomLimit.h);
+    return withinZoomOutLimit;
+  }
+}
+
+// Check to see if the viewBox of the targeting svg is within the limit we define
+// if yes, zoom
+function zoomWithinLimit(scrollDist, targetSvg, zoomInLimit, zoomOutLimit) {
+  var i;
+  if (scrollDist >= 1) { // scroll up to zoom out
+    for (i = 1; i <= scrollDist; i++) {
+      if (checkWithinZoomLimit(targetSvg, "out", zoomOutLimit)) {
+        svgZooming(targetSvg, (1 / svgZoomingRatio));
+      }
+    }
+  }
+  if (scrollDist <= -1) { // scroll down to zoom in
+    for (i = scrollDist; i <= -1; i++) {
+      if (checkWithinZoomLimit(targetSvg, "in", zoomInLimit)) {
+        svgZooming(targetSvg, svgZoomingRatio);
+      }
+    }
+  }
+}
+
+// Apply zoom level
+function svgZooming(target, ratio) {
+  var box = getZoom(target);
+  var newViewBox = generateNewViewBox(target, box, ratio);
+  setZoom(newViewBox, target);
+}
+
+function generateNewViewBox(target, box, ratio) {
+  var oldWidth = box.w;
+  var newWidth = oldWidth / ratio;
+  var offsetX = (newWidth - oldWidth) / 2;
+
+  var oldHeight = box.h;
+  var newHeight = oldHeight / ratio;
+  var offsetY = (newHeight - oldHeight) / 2;
+
+  box.w = box.w / ratio;
+  box.h = box.h / ratio;
+  box.x = box.x - offsetX;
+  box.y = box.y - offsetY;
+
+  return box;
+}
+
+
+
+
+/* Pan by dragging ======================================== */
+
+var onDragGraph = false;
+var graphDragStart = {};
+
+/* vizcanvas */
+document.querySelector(".stage").addEventListener("mousedown", function (event) {
+  if (event.target.mozMatchesSelector(".vizcanvas, .vizcanvas *") && !event.target.mozMatchesSelector(".node, .node *")) {
+    onDragGraph = true;
+    graphDragStart.x = event.clientX;
+    graphDragStart.y = event.clientY;
+  }
+
+}, false);
+
+document.querySelector(".stage").addEventListener("mousemove", function (event) {
+  if (event.target.mozMatchesSelector(".vizcanvas") && !event.target.mozMatchesSelector(".node, .node *") && onDragGraph) {
+    vizcanvas.style.cursor = "-moz-grab";
+    var offsetX = (Math.ceil(event.clientX) - graphDragStart.x);
+    var offsetY = (Math.ceil(event.clientY) - graphDragStart.y);
+    var box = getZoom(vizcanvas);
+    box.x -= (offsetX * box.w / 700);
+    box.y -= (offsetY * box.h / 700);
+    graphDragStart.x += offsetX;
+    graphDragStart.y += offsetY;
+    setZoom(box, vizcanvas);
+  }
+
+}, false);
+
+document.querySelector(".stage").addEventListener("mouseup", function (event) {
+  onDragGraph = false;
+  vizcanvas.style.cursor = "default";
+}, false);
+
+document.querySelector(".stage").addEventListener("mouseleave", function (event) {
+  onDragGraph = false;
+  vizcanvas.style.cursor = "default";
+}, false);
+
+
+/* Legend & Controls ===================================== */
+
+global.toggleLegendSection = function toggleLegendSection(eventTarget, legendElm) {
+  var elmToToggle = legendElm.querySelector(".legend-controls");
+  if (elmToToggle.classList.contains("hidden")) {
+    elmToToggle.classList.remove("hidden");
+    eventTarget.textContent = "Hide";
+  } else {
+    elmToToggle.classList.add("hidden");
+    eventTarget.textContent = "Show";
+  }
+};
+
+global.toggleVizElements = function toggleVizElements(elements, classToggle) {
+  toArray(elements).forEach(function (elm) {
+    elm.classList.toggle(classToggle);
+  });
+};
+
+
+
+/* Glowing Effect for Graph/Clock & Highlighting Effect for List ============= */
+
+global.selectedNodeEffect = function selectedNodeEffect(name) {
+  if (g.currentVisualization.name == "graph") {
+    resetAllGlow("all");
+    addGlow(name, "selected");
+  }
+  if (g.currentVisualization.name == "list") {
+    resetHighlightedRow();
+  }
+};
+
+global.connectedNodeEffect = function connectedNodeEffect(name) {
+  // console.log(name);
+  if (g.currentVisualization.name != "list") {
+    var glow = document.querySelector(".connected-glow");
+    while (glow) {
+      glow = document.querySelector(".connected-glow");
+      glow.parentNode.removeChild(glow);
+    }
+    addGlow(name, "connected");
+  } else {
+    resetHighlightedRow();
+    var row = document.querySelector(".list-table tr[data-name='" + name + "']");
+    if (row) {
+      row.classList.add("selected-connected-row");
+    }
+  }
+
+};
+
+// for Graph & Clock
+global.addGlow = function addGlow(name, type) {
+  type = (type == "selected") ? "selected-glow" : "connected-glow";
+  var viz = g.currentVisualization.name;
+  var gNodes = document.querySelectorAll(".node[data-name='" + name + "']");
+  toArray(gNodes).forEach(function (gNode) {
+    var glowProps = calculateGlowSize(gNode, viz);
+    d3.select(gNode)
+      .insert('circle', ":first-child")
+      .attr('cx', glowProps.cx)
+      .attr('cy', glowProps.cy)
+      .attr('r', glowProps.radius)
+      .attr("fill", "url(#" + type + ")")
+      .classed(type, true);
+
+  });
+};
+
+global.calculateGlowSize = function calculateGlowSize(gNode, viz) {
+  var glowProps = {};
+  var siteNode = gNode.childNodes[0];
+  var shape = siteNode.nodeName.toLowerCase();
+  var radius = graphNodeRadius[g.currentVisualization.name];
+  if (viz == "graph") {
+    if (shape == "polygon") radius *= 2.2;
+    glowProps.radius = radius + 22;
+  } else {
+    glowProps.radius = radius * 4;
+  }
+
+  glowProps.cx = siteNode.getAttribute("cx") || 0;
+  glowProps.cy = siteNode.getAttribute("cy") || 0;
+
+  return glowProps;
+};
+
+// for Graph
+global.resetAllGlow = function resetAllGlow(type) {
+  var selectedGlow;
+  var connectedGlow;
+  if (type == "selected" || type == "all") {
+    while (document.querySelector(".selected-glow")) {
+      selectedGlow = document.querySelector(".selected-glow");
+      selectedGlow.parentNode.removeChild(selectedGlow);
+    }
+  }
+  if (type == "connected" || type == "all") {
+    while (document.querySelector(".connected-glow")) {
+      connectedGlow = document.querySelector(".connected-glow");
+      connectedGlow.parentNode.removeChild(connectedGlow);
+    }
+  }
+};
+
+// for List
+global.resetHighlightedRow = function resetHighlightedRow() {
+  var preHighlighted = document.querySelector(".list-table .selected-connected-row");
+  if (preHighlighted) {
+    preHighlighted.classList.remove("selected-connected-row");
+  }
+};
+
+/**************************************************
+ *   Singular / Plural Noun
+ */
+global.singularOrPluralNoun = function singularOrPluralNoun(num, str) {
+  if (typeof num != "number") {
+    num = parseFloat(num);
+  }
+  return (num > 1) ? str + "s" : str;
+};
+
+function updateUIFromMetadata(event) {
+  document.querySelector('#version-number').textContent = event.version;
+}
+
+function updateUIFromPrefs(event) {
+  if ("contributeData" in event && event.contributeData) {
+    var toggleBtn = document.querySelector(".share-btn");
+    if (event.contributeData) {
+      toggleBtn.querySelector("input").checked = true;
+      toggleBtnOnEffect(toggleBtn);
+    } else {
+      toggleBtn.querySelector("input").checked = false;
+      toggleBtnOffEffect(toggleBtn);
+    }
+  }
+  if ("defaultVisualization" in event) {
+    global.currentVisualization = visualizations[event.defaultVisualization];
+    if (global.currentVisualization) {
+      console.debug("Got viz");
+    } else {
+      console.error("NO viz");
+    }
+  }
+
+  if ("defaultFilter" in event) {
+    aggregate.currentFilter = event.defaultFilter;
+    document.querySelector('a[data-value=' + aggregate.currentFilter + ']')
+      .dataset.selected = true;
+    document.querySelector(".filter-display header").textContent =
+      document.querySelector(".btn_group.session")
+      .querySelector("[data-selected]").textContent;
+  }
+}
+
+// Exports
+global.updateUIFromMetadata = updateUIFromMetadata;
+global.updateUIFromPrefs = updateUIFromPrefs;
+})(this);
diff --git a/resources/lightbeam/data/upgrade.html b/resources/lightbeam/data/upgrade.html
new file mode 100644
index 0000000..25ad182
--- /dev/null
+++ b/resources/lightbeam/data/upgrade.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Welcome to Lightbeam!</title>
+    <link rel="stylesheet" href="OpenSans.css" />
+    <link rel="stylesheet" href="first-run.css" />
+  </head>
+  <body>
+
+    <div id="header">
+      <img src="icons/lightbeam_logo-wordmark_300x150.png" class="center" id="lightbeam-logo" />
+      <h1 class="center">Thanks for upgrading Lightbeam!</h1>
+    </div>
+
+    <ol id="steps">
+      <li>
+        <p><strong>Major changes this release</strong> include an updated
+data format, refactoring the content scripts to omit references to <a
+href="https://blog.mozilla.org/addons/2014/04/10/changes-to-unsafewindow-for-the-add-on-sdk/">unsafeWindow</a>,
+and refactoring storage. <strong>Due to storage changes, previously stored
+connections will be wiped out with this upgrade</strong>. We expect users'
+connections graph to be restored to their normal state in a few days, since
+quota limits restrict the amount of data stored anyway. For a complete list
+of changes, please visit <a
+href=https://github.com/mozilla/lightbeam/compare/1.0.9...1.0.10>our github
+repository</a>.</p>
+     </li>
+      <li>
+        <p><strong>First-time contributors for this release</strong> include Francois Marier, Spyros Livathinos, and Andrew William-Smith. We are very fortunate to have such wonderful contributors in our community.</p>
+      </li>
+      <li>
+        <ul id="contact">
+          <li>Learn more about Mozilla's Lightbeam for Firefox on our <a href="https://mozilla.org/lightbeam">project page</a></li>
+          <li>Question? Idea? Email the mailing list: <a href="mailto:lightbeam-feedback at mozilla.org" target="_blank">lightbeam-feedback at mozilla.org</a></li>
+          <li>Found a bug? <a href="https://github.com/mozilla/lightbeam/issues/new" target="_blank">Report an issue</a> on Github</li>
+          <li>Don't forget to <a href="https://addons.mozilla.org/en-US/firefox/addon/lightbeam/reviews/add" target="_blank">leave a review</a> on our Mozilla Addons page!</li>
+        </ul>
+      </li>
+    </ol>
+
+    <button id="openLightbeam" class="center"><img src="icons/lightbeam_logo-only_16x16.png" /> Open Lightbeam</button>
+
+  </body>
+</html>
diff --git a/resources/lightbeam/lib/connection.js b/resources/lightbeam/lib/connection.js
new file mode 100644
index 0000000..a6bf07c
--- /dev/null
+++ b/resources/lightbeam/lib/connection.js
@@ -0,0 +1,300 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+// Connection object. This module may try to do too many things.
+//
+// Convert an HTTP request (channel) to a loggable, visualizable connection
+// object, if possible
+
+const {
+  Cc, Ci, Cr
+} = require('chrome');
+const Request = require('sdk/request').Request;
+const {
+  on, off, emit
+} = require('sdk/event/core');
+const {
+  data
+} = require("sdk/self");
+const addonUrl = data.url("index.html");
+const persist = require("./persist");
+const {
+  setTimeout
+} = require("sdk/timers");
+const ss = require('sdk/simple-storage');
+// An array of connection objects serialized to an array.
+var connBatch = [];
+const connBatchSize = 200;
+
+var eTLDSvc = Cc["@mozilla.org/network/effective-tld-service;1"].
+getService(Ci.nsIEffectiveTLDService);
+
+const {
+  getTabForChannel
+} = require('./tab/utils');
+
+exports.Connection = Connection;
+exports.addConnection = addConnection;
+
+function addConnection(connection) {
+  connBatch.push(connection.toLog());
+  if (connBatch.length == connBatchSize) {
+    flushToStorage();
+  }
+  console.debug("got", connBatch.length, "connections");
+}
+
+exports.getAllConnections = function getAllConnections() {
+  console.debug("got", connBatch.length, "buffered connections", ss.storage.connections.length, "persisted connections");
+  return ss.storage.connections.concat(connBatch);
+};
+
+function excludePrivateConnections(connections) {
+  return connections.filter(function (connection) {
+    return !connection[Connection.FROM_PRIVATE_MODE];
+  });
+}
+
+function flushToStorage() {
+  console.debug("flushing", connBatch.length, "buffered connections");
+  persist.storeConnections(excludePrivateConnections(connBatch));
+  connBatch.length = 0;
+}
+// Every 5 minutes, flush to storage.
+setTimeout(flushToStorage, 5 * 60 * 1000);
+
+// Get eTLD + 1 (e.g., example.com from foo.example.com)
+function getDomain(host) {
+  try {
+    return eTLDSvc.getBaseDomainFromHost(host);
+  } catch (e
+    if e.result === Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
+    return false;
+  } catch (e
+    if e.result === Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {
+    return false;
+  } catch (e) {
+    console.error('getDomain(): unexpected error: ' + host + ': ' + e);
+    throw e;
+  }
+}
+
+Connection.getDomain = getDomain; // make it part of what we export
+Connection.reset = function () {
+  connBatch.length = 0;
+};
+
+// Get subdomain (e.g., foo from foo.example.com)
+function getSubdomain(host) {
+  var domain = getDomain(host);
+  return host.slice(0, host.length - domain.length);
+}
+
+function Connection() {}
+
+// subject comes from events.on("http-on-modify-request");
+Connection.fromSubject = function (subject) {
+  var conn = new Connection();
+  conn.restoreFromSubject(subject);
+  return conn;
+};
+
+// Functions below may include legacy code from the first version of Collusion.
+// Find the page the URL was originally loaded from to determine whether this
+// happened in first or third-party context.
+function getAjaxRequestHeader(channel) {
+  var header = null;
+  try {
+    header = channel.getRequestHeader('X-Requested-With').toLowerCase() === 'xmlhttprequest';
+  } catch (e) {
+    if (e.name === 'NS_ERROR_NOT_AVAILABLE') {
+      /* header not found, do nothing */
+    } else {
+      console.error('what is this? ' + Object.keys(e).join(','));
+      throw e;
+    }
+  }
+  return header;
+}
+
+// Does this make sense from http-on-examine-response? The response will have
+// Set-Cookie headers, the request will have Cookie headers. We should
+// investigate nsIHttpChannel to make sure that the response channel has the
+// original request headers.
+function hasCookie(channel) {
+  try {
+    return !!channel.getRequestHeader('Cookie');
+  } catch (e) {
+    return false;
+  }
+}
+
+function getProtocol(uri) {
+  return uri.scheme;
+}
+
+// See doc/Heuristic for determining first party-ness.md
+Connection.prototype.restoreFromSubject = function (event) {
+  // Check to see if this is in fact a third-party connection, if not, return
+  var channel = event.subject.QueryInterface(Ci.nsIHttpChannel);
+  // The referrer may not be set properly, especially in the case where HTTPS
+  // includes HTTP where the referrer is stripped due to mixed-content
+  // attacks. Also it doesn't work for RTC or WebSockets, but since we are
+  // only examining HTTP requests right now, that's all right.
+  var source = channel.referrer;
+  var target = channel.URI;
+  var targetDomain = getDomain(target.host);
+  var tab = null;
+  try {
+    tab = getTabForChannel(channel);
+  } catch (e) {
+    console.debug('EXCEPTION CAUGHT: No tab for connection');
+    tab = null;
+  }
+  var isAjax = getAjaxRequestHeader(channel);
+  var valid = true;
+  var browserUri = tab ? tab.linkedBrowser.currentURI : null;
+  var browserSpec = browserUri && browserUri.spec;
+  var browserDomain = null;
+  try {
+    browserDomain = browserUri && getDomain(browserUri.host);
+  } catch (e) {
+    // chances are the URL is about:blank, which has no host and throws an exception
+    // console.error('Error getting host from: ' + browserUri.spec);
+  }
+  // This is probably the largest source of false positives.
+  var sourceVisited = !isAjax && (browserDomain === targetDomain || browserSpec === 'about:blank');
+  // Connection.log('browserUri ' + browserUri.spec + (sourceVisited ? ' equals ' : ' does not equal') + ' target ' + ( target && target.spec));
+  if (sourceVisited) {
+    source = target;
+  } else if (!source) {
+    // This may introduce faulty data.
+    // console.error('No source for target ' + target.spec + ' (' + channel.referrer  + ')');
+    source = target; // can't have a null source
+  }
+  var sourceDomain = getDomain(source.host);
+  var cookie = hasCookie(channel);
+  var sourceProtocol = getProtocol(source);
+  var targetProtocol = getProtocol(target);
+  var isSecure = targetProtocol === 'https';
+  var isPrivate = tab && tab.isPrivate;
+  // Handle all failure conditions
+  if (browserUri && browserUri.spec === addonUrl) {
+    this.valid = false;
+    this.message = 'Do not record connections made by this add-on';
+    // console.error(this.message);
+    return this;
+  }
+  if (!sourceDomain) {
+    this.valid = false;
+    this.message = 'Invalid source domain: ' + source.host;
+    // console.error(this.message);
+    return this;
+  }
+  if (!targetDomain) {
+    this.valid = false;
+    this.message = 'Invalid target domain: ' + target.host;
+    // console.error(this.message);
+    return this;
+  }
+  if (target.host === 'localhost' || source.host === 'localhost') {
+    this.valid = false;
+    this.message = 'Localhost is not trackable';
+    // console.error(this.message);
+    return this;
+  }
+  if (sourceProtocol === 'http' || sourceProtocol === 'https' ||
+    targetProtocol === 'http' || targetProtocol === 'https') {
+    /* OK, do nothing */
+  } else {
+    this.valid = false;
+    this.message = 'Unsupported protocol: ' + sourceProtocol + ' -> ' + targetProtocol;
+    // console.error(this.message);
+    return this;
+  }
+  if (!tab) {
+    this.valid = false;
+    this.message = 'No tab found for request: ' + target.spec + ',  isAjax: ' + isAjax;
+    // console.error(this.message);
+    return this;
+  }
+  // set instance values for return
+  this.valid = true;
+  this.source = sourceDomain;
+  this.target = targetDomain;
+  this.timestamp = Date.now();
+  this.contentType = channel.contentType || 'text/plain';
+  this.cookie = cookie;
+  // The client went to this page in a first-party context.
+  this.sourceVisited = sourceVisited;
+  this.secure = isSecure;
+  this.sourcePathDepth = source.path.split('/').length - 1;
+  this.sourceQueryDepth = source.query ? target.query.split(/;|\&/).length : 0;
+  this.sourceSub = getSubdomain(source.host);
+  this.targetSub = getSubdomain(target.host);
+  this.method = channel.requestMethod;
+  this.status = channel.responseStatus;
+  this.cacheable = !channel.isNoCacheResponse();
+  // We visualize private connections but never store them.
+  this.isPrivate = isPrivate;
+  this._sourceTab = tab; // Never logged, only for associating data with current tab
+  // console.error((sourceVisited ? 'site: ' : 'tracker: ') + sourceDomain + ' -> ' + targetDomain + ' (' + browserUri.spec + ')');
+};
+
+// Connection - level methods (not on instances)
+// This may be supported by the addon-sdk events.on now.
+Connection.on = function (eventname, handler) {
+  on(Connection, eventname, handler);
+};
+
+Connection.off = function (eventname) {
+  off(Connection, eventname);
+};
+
+Connection.emit = function (eventname, arg1, arg2, arg3) {
+  emit(Connection, eventname, arg1, arg2, arg3);
+};
+
+// Constants for indexes of properties in array format
+Connection.SOURCE = 0;
+Connection.TARGET = 1;
+Connection.TIMESTAMP = 2;
+Connection.CONTENT_TYPE = 3;
+Connection.COOKIE = 4;
+Connection.SOURCE_VISITED = 5;
+Connection.SECURE = 6;
+Connection.SOURCE_PATH_DEPTH = 7;
+Connection.SOURCE_QUERY_DEPTH = 8;
+Connection.SOURCE_SUB = 9;
+Connection.TARGET_SUB = 10;
+Connection.METHOD = 11;
+Connection.STATUS = 12;
+Connection.CACHEABLE = 13;
+Connection.FROM_PRIVATE_MODE = 14;
+
+Connection.prototype.toLog = function () {
+  if (!this.valid) {
+    throw new Error('Do not log invalid connections: ' + this.message);
+  }
+  var theLog = [
+    this.source,
+    this.target,
+    this.timestamp.valueOf(),
+    this.contentType,
+    this.cookie,
+    this.sourceVisited,
+    this.secure,
+    this.sourcePathDepth,
+    this.sourceQueryDepth,
+    this.sourceSub,
+    this.targetSub,
+    this.method,
+    this.status,
+    this.cacheable,
+    this._sourceTab.isPrivate
+  ];
+  if (this.isPrivate) {
+    theLog.push(this.isPrivate);
+  }
+  return theLog;
+};
diff --git a/resources/lightbeam/lib/main.js b/resources/lightbeam/lib/main.js
new file mode 100644
index 0000000..cbef4fe
--- /dev/null
+++ b/resources/lightbeam/lib/main.js
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* global exports, require */
+"use strict";
+
+const events = require("sdk/system/events");
+const {
+  PageMod
+} = require("sdk/page-mod");
+const {
+  data
+} = require("sdk/self");
+
+const {
+  Connection, addConnection
+} = require('./connection');
+const tabEvents = require('./tab/events');
+const ui = require('./ui');
+const persist = require("./persist");
+
+var tabs = require("sdk/tabs");
+
+// This is the heart of Lightbeam, we get all of our data from observing these
+// requests.
+events.on("http-on-examine-response", function (subject) {
+  var connection = Connection.fromSubject(subject);
+  if (connection.valid) {
+    addConnection(connection);
+    // Pass the message on to the UI
+    ui.emitForWorker('connection', connection.toLog());
+  }
+}, true);
+
+// This lets us hook into page load events and communicate to page workers.
+PageMod({
+  include: ui.mainPage,
+  contentScriptWhen: 'ready',
+  contentScriptFile: [
+    data.url('content-script.js'),
+    data.url('d3/d3.min.js'),
+    data.url('events.js'),
+    data.url('infobar.js'),
+    data.url('lightbeam.js'),
+    data.url('svgdataset.js'),
+    data.url('aggregate.js'),
+    data.url('picoModal/picoModal-1.0.0.min.js'),
+    data.url('tooltip.js'),
+    data.url('dialog.js'),
+    data.url('ui.js'),
+    data.url('parseuri.js'),
+    data.url('graph.js'),
+    data.url('list.js'),
+  ],
+  onAttach: ui.attachToLightbeamPage
+});
+
+exports.main = function (options, callbacks) {
+  let initialPage = null;
+  switch (options.loadReason) {
+    case "install":
+      initialPage = "first-run.html";
+      break;
+    //case "upgrade":
+    //  initialPage = "upgrade.html";
+    //  break;
+  }
+  if (initialPage) {
+    let initialPageUrl = data.url(initialPage);
+    tabs.open(initialPageUrl);
+    // Add a content script to open lightbeam if the corresponding button is
+    // pressed in the inital page
+    PageMod({
+      include: initialPageUrl,
+      contentScriptWhen: 'ready',
+      contentScriptFile: data.url('initialPage.js'),
+      onAttach: function (worker) {
+        worker.port.on('openLightbeam', ui.openOrSwitchToOrClose);
+      }
+    });
+  }
+};
diff --git a/resources/lightbeam/lib/persist.js b/resources/lightbeam/lib/persist.js
new file mode 100644
index 0000000..8d49770
--- /dev/null
+++ b/resources/lightbeam/lib/persist.js
@@ -0,0 +1,153 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* global console, require, exports */
+// All writes to storage and upload logic in the addon process goes here.
+"use strict";
+
+const ss = require('sdk/simple-storage');
+const Request = require("sdk/request").Request;
+const prefs = require("sdk/simple-prefs").prefs;
+
+var storage = ss.storage;
+
+// Only these keys may exist as maps on ss.storage. Everything else is legacy.
+const STORAGE_KEYS = [
+  "blockmap",
+  "connections",
+];
+
+// Upload logic.
+function serializeConnections(connections) {
+  let exportSet = {
+    format: 'Lightbeam Save File',
+    version: '1.2',
+    userId: storage.userId,
+    userAgentData: getUserAgentData(),
+    uploadTime: Date.now(),
+    connections: connections
+  };
+  console.debug(JSON.stringify(exportSet));
+  return JSON.stringify(exportSet);
+}
+
+function getUserAgentData() {
+  let retval = {};
+  let app = require("sdk/system/xul-app");
+  retval.appname = app.name;
+  retval.fxVersion = app.version;
+  let prefService = require("sdk/preferences/service");
+  let prefs = [ "app.update.channel",
+                "network.cookie.behavior",
+                "privacy.donottrackheader.enabled",
+                "privacy.donottrack.header.value" ];
+  prefs.forEach(function(p) { retval[p] = prefService.get(p); });
+  retval.addons = getAddons();
+  console.debug(JSON.stringify(retval));
+  return retval;
+}
+
+function getAddons() {
+  const { Cu } = require('chrome');
+  let { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm");
+  let addons = [];
+  AddonManager.getAllAddons(function(addonList) {
+    addonList.forEach(function(addon) {
+      let o = {};
+      let states = ['id', 'name', 'appDisabled', 'isActive', 'type',
+                    'userDisabled'];
+      states.forEach(function(state) { o[state] = addon[state]; });
+      addons.push(o);
+    });
+  });
+  console.debug(JSON.stringify(addons));
+  return addons;
+}
+
+function upload(connections) {
+  console.debug("received upload event in addon");
+  let uploadServer = 'https://data.mozilla.com/submit/lightbeam';
+  let request = Request({
+    url: uploadServer,
+    contentType: "application/json",
+    onComplete: function (response) {
+      let status = Number(response.status);
+      if (status >= 200 && status < 300) {
+        console.log("successful upload: ", response.text);
+      } else {
+        // Ignore errors for now. We could save last upload time and try
+        // uploading again the ones that failed previously.
+        console.log("error uploading: ", status, response.text);
+      }
+    },
+    content: serializeConnections(connections)
+  });
+  request.post();
+}
+
+// Delete oldest connections. When we hit the simple storage quota limit,
+// Firefox throws an exception that the user won't see.  We tried switching to
+// indexdb (which is async) but abandoned it. localForage may be a viable
+// substitute.
+function checkStorageQuota() {
+  while (ss.quotaUsage > 1) {
+    var sliceStart = ss.storage.connections.length / 2;
+    ss.storage.connections = ss.storage.connections.slice(sliceStart);
+  }
+}
+
+// Flush connections to simple-storage.
+exports.storeConnections = function storeConnections(connections) {
+  checkStorageQuota();
+  storage.connections = storage.connections.concat(connections);
+  if (prefs.contributeData) {
+    upload(connections);
+  }
+};
+
+// Reset stored state, including preferences
+exports.reset = function reset() {
+  storage.connections.length = 0;
+  storage.blockmap = {};
+  storage.userId = generateUserId();
+  prefs.contributeData = false;
+  prefs.defaultVisualization = "graph";
+  prefs.defaultFilter = "daily";
+};
+
+// Generate a new user id.
+function generateUserId() {
+  // Short hex string.
+  let userId = Math.floor(0xFFFFFFFF * Math.random()).toString(16);
+  storage.userId = userId + ":" + Date.now();
+  return storage.userId;
+}
+
+// Possibly rotate the user id.
+function maybeRotateUserId(forceChange) {
+  let parts = storage.userId.split(":");
+  // 90 days in ms
+  let MAX_LIFETIME_MS = 90 * 24 * 60 * 60 * 1000;
+  let timeToChange = Date(parts[1] + MAX_LIFETIME_MS);
+  if (forceChange || Date.now() >= timeToChange) {
+    generateUserId();
+  }
+}
+
+// Initialize all of our storage
+if (!storage.connections) {
+  storage.connections = [];
+}
+
+if (!storage.blockmap) {
+  storage.blockmap = {};
+}
+
+if (!storage.userId) {
+  generateUserId();
+}
+
+// Rotate user id if necessary
+maybeRotateUserId();
+
+console.log('Current quota usage:', Math.round(ss.quotaUsage * 100));
diff --git a/resources/lightbeam/lib/shared/menuitems.js b/resources/lightbeam/lib/shared/menuitems.js
new file mode 100644
index 0000000..049b1e4
--- /dev/null
+++ b/resources/lightbeam/lib/shared/menuitems.js
@@ -0,0 +1,244 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* global require, exports */
+'use strict';
+
+const windowUtils = require("sdk/deprecated/window-utils");
+const {
+  Class
+} = require("sdk/core/heritage");
+const {
+  validateOptions
+} = require("sdk/deprecated/api-utils");
+const {
+  on, emit, once, off
+} = require("sdk/event/core");
+const {
+  isBrowser
+} = require("sdk/window/utils");
+const {
+  EventTarget
+} = require('sdk/event/target');
+const {
+  unload
+} = require('./unload+');
+
+const menuitemNS = require("sdk/core/namespace").ns();
+const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+function MenuitemOptions(options) {
+  return validateOptions(options, {
+    id: {
+      is: ['string']
+    },
+    menuid: {
+      is: ['undefined', 'string']
+    },
+    insertbefore: {
+      is: ['undefined', 'string', 'object', 'number']
+    },
+    label: {
+      is: ["string"]
+    },
+    include: {
+      is: ['string', 'undefined']
+    },
+    disabled: {
+      is: ["undefined", "boolean"],
+      map: function (v) !! v
+    },
+    accesskey: {
+      is: ["undefined", "string"]
+    },
+    key: {
+      is: ["undefined", "string"]
+    },
+    checked: {
+      is: ['undefined', 'boolean']
+    },
+    className: {
+      is: ["undefined", "string"]
+    },
+    onCommand: {
+      is: ['undefined', 'function']
+    },
+    useChrome: {
+      map: function (v) !! v
+    }
+  });
+}
+
+let Menuitem = Class({
+  extends: EventTarget,
+  initialize: function (options) {
+    options = menuitemNS(this).options = MenuitemOptions(options);
+    EventTarget.prototype.initialize.call(this, options);
+
+    menuitemNS(this).destroyed = false;
+    menuitemNS(this).unloaders = [];
+    menuitemNS(this).menuitems = addMenuitems(this, options).menuitems;
+  },
+  get id() menuitemNS(this).options.id,
+  get label() menuitemNS(this).options.label,
+  set label(val) updateProperty(this, 'label', val),
+  get checked() menuitemNS(this).options.checked,
+  set checked(val) updateProperty(this, 'checked', !! val),
+  get disabled() menuitemNS(this).options.disabled,
+  set disabled(val) updateProperty(this, 'disabled', !! val),
+  get key() menuitemNS(this).options.key,
+  set key(val) updateProperty(this, 'key', val),
+  clone: function (overwrites) {
+    let opts = Object.clone(menuitemNS(this).options);
+    for (let key in overwrites) {
+      if (overwrites.hasOwnProperty(key)) {
+        opts[key] = overwrites[key];
+      }
+    }
+    return Menuitem(opts);
+  },
+  get menuid() menuitemNS(this).options.menuid,
+  set menuid(val) {
+    let options = menuitemNS(this).options;
+    options.menuid = val;
+
+    forEachMI(function (menuitem, i, $) {
+      updateMenuitemParent(menuitem, options, $);
+    });
+  },
+  destroy: function () {
+    if (!menuitemNS(this).destroyed) {
+      menuitemNS(this).destroyed = true;
+      menuitemNS(this).unloaders.forEach(function (u) u());
+      menuitemNS(this).unloaders = null;
+      menuitemNS(this).menuitems = null;
+    }
+    return true;
+  }
+});
+
+function addMenuitems(self, options) {
+  let menuitems = [];
+
+  // setup window tracker
+  windowUtils.WindowTracker({
+    onTrack: function (window) {
+      if (menuitemNS(self).destroyed) return;
+      if (options.include) {
+        if (options.include != window.location) return;
+      } else if (!isBrowser(window)) {
+        return;
+      }
+
+      // add the new menuitem to a menu
+      var menuitem = updateMenuitemAttributes(
+        window.document.createElementNS(NS_XUL, "menuitem"), options);
+      var menuitems_i = menuitems.push(menuitem) - 1;
+
+      // add the menutiem to the ui
+      let added = updateMenuitemParent(menuitem, options, function (id) window.document.getElementById(id));
+
+      menuitem.addEventListener("command", function () {
+        if (!self.disabled)
+          emit(self, 'command', options.useChrome ? window : null);
+      }, true);
+
+      // add unloader
+      let unloader = function unloader() {
+        if (menuitem.parentNode) {
+          menuitem.parentNode.removeChild(menuitem);
+        }
+        menuitems[menuitems_i] = null;
+      };
+      let remover = unload(unloader, window);
+      menuitemNS(self).unloaders.push(function () {
+        remover();
+        unloader();
+      });
+    }
+  });
+  return {
+    menuitems: menuitems
+  };
+}
+
+function updateMenuitemParent(menuitem, options, $) {
+  // add the menutiem to the ui
+  if (Array.isArray(options.menuid)) {
+    let ids = options.menuid;
+    for (var len = ids.length, i = 0; i < len; i++) {
+      if (tryParent($(ids[i]), menuitem, options.insertbefore))
+        return true;
+    }
+  } else {
+    return tryParent($(options.menuid), menuitem, options.insertbefore);
+  }
+  return false;
+}
+
+function updateMenuitemAttributes(menuitem, options) {
+  menuitem.setAttribute("id", options.id);
+  menuitem.setAttribute("label", options.label);
+
+  if (options.accesskey)
+    menuitem.setAttribute("accesskey", options.accesskey);
+
+  if (options.key)
+    menuitem.setAttribute("key", options.key);
+
+  menuitem.setAttribute("disabled", !! options.disabled);
+
+  if (options.image) {
+    menuitem.classList.add("menuitem-iconic");
+    menuitem.style.listStyleImage = "url('" + options.image + "')";
+  }
+
+  if (options.checked)
+    menuitem.setAttribute('checked', options.checked);
+
+  if (options.className)
+    options.className.split(/\s+/).forEach(function (name) menuitem.classList.add(name));
+
+  return menuitem;
+}
+
+function updateProperty(menuitem, key, val) {
+  menuitemNS(menuitem).options[key] = val;
+
+  forEachMI(function (menuitem) {
+    menuitem.setAttribute(key, val);
+  }, menuitem);
+  return val;
+}
+
+function forEachMI(callback, menuitem) {
+  menuitemNS(menuitem).menuitems.forEach(function (mi, i) {
+    if (!mi) return;
+    callback(mi, i, function (id) mi.ownerDocument.getElementById(id));
+  });
+}
+
+function tryParent(parent, menuitem, before) {
+  if (parent) parent.insertBefore(menuitem, insertBefore(parent, before));
+  return !!parent;
+}
+
+function insertBefore(parent, before) {
+  if (typeof before == "number") {
+    switch (before) {
+    case MenuitemExport.FIRST_CHILD:
+      return parent.firstChild;
+    }
+    return null;
+  } else if (typeof before == "string") {
+    return parent.querySelector("#" + before);
+  }
+  return before;
+}
+
+function MenuitemExport(options) {
+  return Menuitem(options);
+}
+MenuitemExport.FIRST_CHILD = 1;
+
+exports.Menuitem = MenuitemExport;
diff --git a/resources/lightbeam/lib/shared/policy.js b/resources/lightbeam/lib/shared/policy.js
new file mode 100644
index 0000000..22188f4
--- /dev/null
+++ b/resources/lightbeam/lib/shared/policy.js
@@ -0,0 +1,155 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* global require, exports */
+'use strict';
+
+const {
+  Cc, Ci
+} = require('chrome');
+const {
+  Class
+} = require('sdk/core/heritage');
+const CP_NS = require('sdk/core/namespace').ns();
+const {
+  ensure
+} = require('sdk/system/unload');
+const {
+  validateOptions
+} = require('sdk/deprecated/api-utils');
+const {
+  id: ADDON_ID
+} = require('sdk/self');
+const xpcom = require('sdk/platform/xpcom');
+
+const CM = Cc["@mozilla.org/categorymanager;1"]
+  .getService(Ci.nsICategoryManager);
+
+const ACCEPT = exports.ACCEPT = Ci.nsIContentPolicy.ACCEPT;
+const REJECT = exports.REJECT = Ci.nsIContentPolicy.REJECT_REQUEST;
+
+const accept = function () ACCEPT;
+
+let ContentPolicy_ID = 0;
+
+const RULES = {
+  description: {
+    map: function (v) {
+      return v ? v : '';
+    },
+    is: ['string']
+  },
+  contract: {
+    map: function (v) {
+      if (v === undefined) {
+        v = '@erikvold.com/content-policy.' + ADDON_ID + ';' + ContentPolicy_ID++;
+      }
+      return v;
+    },
+    is: ['string']
+  },
+  entry: {
+    is: ['string', 'undefined']
+  },
+  shouldLoad: {
+    is: ['function', 'undefined']
+  },
+  shouldProcess: {
+    is: ['function', 'undefined']
+  },
+};
+
+function getType(aType) {
+  switch (aType) {
+  case Ci.nsIContentPolicy.TYPE_SCRIPT:
+    return 'script';
+  case Ci.nsIContentPolicy.TYPE_IMAGE:
+    return 'image';
+  case Ci.nsIContentPolicy.TYPE_STYLESHEET:
+    return 'stylesheet';
+  case Ci.nsIContentPolicy.TYPE_OBJECT:
+    return 'object';
+  case Ci.nsIContentPolicy.TYPE_DOCUMENT:
+    return 'document';
+  case Ci.nsIContentPolicy.TYPE_SUBDOCUMENT:
+    return 'subdocument';
+  case Ci.nsIContentPolicy.TYPE_REFRESH:
+    return 'refresh';
+  case Ci.nsIContentPolicy.TYPE_XBL:
+    return 'xbl';
+  case Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST:
+    return 'xhr';
+  case Ci.nsIContentPolicy.TYPE_PING:
+    return 'ping';
+    // TODO: support more types
+  }
+  return 'other';
+}
+const getTypeMemod = memoize(getType, 12, 1);
+
+function makeDetails(aContentType, aContentLocation, aRequestOrigin, aContext, aMimeTypeGuess) {
+  return {
+    type: getTypeMemod(aContentType),
+    location: aContentLocation,
+    origin: aRequestOrigin.spec,
+    context: null, // TODO: support this in a safe way somehow..
+    mimeTypeGuess: String(aMimeTypeGuess)
+  };
+}
+
+let ContentPolicy = exports.ContentPolicy = Class({
+  initialize: function (options) {
+    const self = this;
+    options = CP_NS(self).options = validateOptions(options, RULES);
+    CP_NS(self).shouldLoad = options.shouldLoad || accept;
+    CP_NS(self).shouldProcess = options.shouldProcess || accept;
+
+    let factory = CP_NS(this).factory = xpcom.Factory({
+      Component: getProvider(self),
+      description: options.description,
+      contract: options.contract
+    });
+
+    let entry = options.entry || options.contract;
+    CM.addCategoryEntry('content-policy', entry, factory.contract, false, true);
+    ensure(this, 'destroy');
+  },
+  destroy: function () {
+    // already destroyed?
+    if (!CP_NS(this).options)
+      return;
+
+    let options = CP_NS(this).options;
+    CP_NS(this).options = null;
+    CP_NS(this).shouldLoad = accept;
+    CP_NS(this).shouldProcess = accept;
+
+    CM.deleteCategoryEntry('content-policy', options.entry || options.contract, false);
+  }
+});
+
+function getProvider(self) {
+  return Class({
+    extends: xpcom.Unknown,
+    interfaces: ['nsIContentPolicy'],
+    shouldLoad: function (aContentType, aContentLocation, aRequestOrigin, aContext, aMimeTypeGuess, aExtra) {
+      let load = CP_NS(self).shouldLoad(makeDetails.apply(null, arguments));
+      return (load == REJECT || (!load && load !== undefined)) ? REJECT : ACCEPT;
+    },
+    shouldProcess: function (aContentType, aContentLocation, aRequestOrigin, aContext, aMimeTypeGuess, aExtra) {
+      let load = CP_NS(self).shouldProcess(makeDetails.apply(null, arguments));
+      return (load == REJECT || (!load && load !== undefined)) ? REJECT : ACCEPT;
+    }
+  });
+}
+
+function memoize(func) {
+  let cache = Object.create(null);
+  return function (a) {
+    let key = a.toString();
+    if (!(key in cache)) {
+      cache[key] = func.call(null, a);
+    }
+    return cache[key];
+  };
+}
diff --git a/resources/lightbeam/lib/shared/unload+.js b/resources/lightbeam/lib/shared/unload+.js
new file mode 100644
index 0000000..51d950d
--- /dev/null
+++ b/resources/lightbeam/lib/shared/unload+.js
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* global require, console, exports */
+'use strict';
+
+const {
+  Class
+} = require("sdk/core/heritage");
+const unloadNS = require("sdk/core/namespace").ns();
+const {
+  when: unload
+} = require("sdk/system/unload");
+
+var Unloader = exports.Unloader = Class({
+  initialize: function Unloader() {
+    unloadNS(this).unloaders = [];
+    unloadNS(this).unloadersUnload = unloadersUnload.bind(null, unloadNS(this).unloaders);
+
+    // run the unloaders on unload
+    unload(unloadNS(this).unloadersUnload);
+  },
+  unload: function unload(callback, container) {
+    // Calling with no arguments runs all the unloader callbacks
+    if (!callback) {
+      unloadNS(this).unloadersUnload();
+      return null;
+    }
+
+    let windowRemover = windowUnloader.bind(null, unloader, unloadNS(this).unloaders);
+
+    // The callback is bound to the lifetime of the container if we have one
+    if (container) {
+      // Remove the unloader when the container unloads
+      container.addEventListener("unload", windowRemover, false);
+
+      // Wrap the callback to additionally remove the unload listener
+      let origCallback = callback;
+      callback = function () {
+        container.removeEventListener("unload", windowRemover, false);
+        origCallback();
+      };
+    }
+
+    // Wrap the callback in a function that ignores failures
+    function unloader() {
+      try {
+        callback();
+      } catch (e) {
+        console.error(e);
+      }
+    }
+    unloadNS(this).unloaders.push(unloader);
+
+    // Provide a way to remove the unloader
+    return removeUnloader.bind(null, unloader, unloadNS(this).unloaders);
+  }
+});
+
+function sliceUnloader(unloader, unloaders) {
+  let index = unloaders.indexOf(unloader);
+  if (index < 0)
+    return [];
+  return unloaders.splice(index, 1);
+}
+// wraps sliceUnloader and doesn't return anything
+function removeUnloader(unloader, unloaders) {
+  sliceUnloader.apply(null, arguments);
+}
+
+function windowUnloader(unloader, unloaders) {
+  sliceUnloader.apply(null, arguments).forEach(function (u) u());
+}
+
+function unloadersUnload(unloaders) {
+  // run all the pending unloaders
+  unloaders.slice().forEach(function (u) u());
+  // clear the unload array
+  unloaders.length = 0;
+}
+
+/**
+ * Save callbacks to run when unloading. Optionally scope the callback to a
+ * container, e.g., window. Provide a way to run all the callbacks.
+ *
+ * @usage unload(): Run all callbacks and release them.
+ *
+ * @usage unload(callback): Add a callback to run on unload.
+ * @param [function] callback: 0-parameter function to call on unload.
+ * @return [function]: A 0-parameter function that undoes adding the callback.
+ *
+ * @usage unload(callback, container) Add a scoped callback to run on unload.
+ * @param [function] callback: 0-parameter function to call on unload.
+ * @param [node] container: Remove the callback when this container unloads.
+ * @return [function]: A 0-parameter function that undoes adding the callback.
+ */
+const gUnload = Unloader();
+exports.unload = gUnload.unload.bind(gUnload);
diff --git a/resources/lightbeam/lib/tab/events.js b/resources/lightbeam/lib/tab/events.js
new file mode 100644
index 0000000..0ecea20
--- /dev/null
+++ b/resources/lightbeam/lib/tab/events.js
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* global require, exports */
+// Simple onTab handler to figure out what tab a connection corresponds to.
+'use strict';
+
+const tabs = require('sdk/tabs');
+const {
+  getTabInfo
+} = require('./utils');
+
+function onTab(eventname, fn) {
+  tabs.on(eventname, function (jptab) {
+    var tabinfo = getTabInfo(jptab);
+    fn(tabinfo);
+  });
+}
+
+exports.on = onTab;
diff --git a/resources/lightbeam/lib/tab/utils.js b/resources/lightbeam/lib/tab/utils.js
new file mode 100644
index 0000000..cfeb6f5
--- /dev/null
+++ b/resources/lightbeam/lib/tab/utils.js
@@ -0,0 +1,118 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+// ChromeTab
+//
+// This is a module for getting the tab a channel is loaded in, from the channel
+
+exports.getTabForChannel = getTabForChannel;
+exports.on = onTab;
+exports.getTabInfo = getTabInfo;
+
+const {
+  Cc, Ci, Cr, components
+} = require('chrome');
+const tabs = require('sdk/tabs');
+const winutils = require('sdk/window/utils');
+const {
+  getTabForContentWindow, getBrowserForTab
+} = require('sdk/tabs/utils');
+
+var wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
+
+components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+// return a variety of info on the tab
+function getTabInfo(jpTab) {
+  // Some windows don't have performance initialized (because they haven't been reloaded since the plugin was initialized?
+  try {
+    var chromeWindow = wm.getMostRecentWindow('navigator:browser');
+    var gBrowser = chromeWindow.gBrowser;
+    var window = gBrowser.contentWindow.wrappedJSObject;
+    return {
+      gBrowser: gBrowser,
+      tab: gBrowser.selectedTab,
+      document: gBrowser.contentDocument,
+      window: window,
+      title: gBrowser.contentTitle, // nsIPrincipal
+      principal: gBrowser.contentPrincipal, // security context
+      uri: gBrowser.contentURI, // nsURI .spec to get string representation
+      loadTime: window.performance.timing.responseStart // milliseconds at which page load was initiated
+    };
+  } catch (e) {
+    return null;
+  }
+}
+
+function onTab(eventname, fn) {
+  tabs.on(eventname, function (jptab) {
+    var tabinfo = getTabInfo(jptab);
+    fn(tabinfo);
+  });
+}
+
+
+// Below code is based on adhacker, taken from http://forums.mozillazine.org/viewtopic.php?f=19&p=6335275
+// Erik Vold may have the most current information on this.
+function getTabForChannel(aHttpChannel) {
+  var loadContext = getLoadContext(aHttpChannel);
+  if (!loadContext) {
+    // fallback
+    return getTabForChannel2(aHttpChannel);
+  }
+  var win = loadContext.topWindow;
+  if (win) {
+    var tab = getTabForContentWindow(win);
+    // http://developer.mozilla.org/en/docs/XUL:tab
+    tab.isPrivate = PrivateBrowsingUtils.isWindowPrivate(win);
+    return tab;
+  } else {
+    // console.error('getTabForChannel() no topWindow found');
+    return null;
+  }
+}
+
+// Special case in case we don't have a load context.
+function getTabForChannel2(aChannel) {
+  var win = getWindowForChannel(aChannel);
+  if (!win) return null;
+
+  var tab = getTabForContentWindow(win);
+  return tab;
+}
+
+function getLoadContext(aRequest) {
+  try {
+    // first try the notification callbacks
+    var loadContext = aRequest.QueryInterface(Ci.nsIChannel)
+      .notificationCallbacks.getInterface(Ci.nsILoadContext);
+    return loadContext;
+  } catch (err1) {
+    // fail over to trying the load group
+    try {
+      if (!aRequest.loadGroup) return null;
+
+      var loadContext = aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
+      return loadContext;
+    } catch (err2) {
+      return null;
+    }
+  }
+}
+
+function getWindowForChannel(aRequest) {
+  var oHttp = aRequest.QueryInterface(Ci.nsIHttpChannel);
+
+  if (!oHttp.notificationCallbacks) {
+    console.log("HTTP request missing callbacks: " + oHttp.originalURI.spec);
+    return null;
+  }
+  var interfaceRequestor = oHttp.notificationCallbacks.QueryInterface(Ci.nsIInterfaceRequestor);
+
+  try {
+    return interfaceRequestor.getInterface(Ci.nsIDOMWindow);
+  } catch (e) {
+    console.log("Failed to to find nsIDOMWindow from interface requestor");
+    return null;
+  }
+}
diff --git a/resources/lightbeam/lib/ui.js b/resources/lightbeam/lib/ui.js
new file mode 100644
index 0000000..b21ff74
--- /dev/null
+++ b/resources/lightbeam/lib/ui.js
@@ -0,0 +1,295 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* global require, exports, console */
+'use strict';
+
+const self = require("sdk/self");
+const data = self.data;
+const tabs = require('sdk/tabs');
+const {
+  isPrivate
+} = require("sdk/private-browsing");
+const {
+  ContentPolicy
+} = require('shared/policy');
+const ss = require('sdk/simple-storage');
+const {
+  on, once, off, emit
+} = require("sdk/event/core");
+const prefs = require("sdk/simple-prefs").prefs;
+
+const persist = require("./persist");
+const {
+  Connection, getAllConnections
+} = require("./connection");
+
+const xulapp = require("sdk/system/xul-app");
+const usingAustralis = xulapp.satisfiesVersion(">=29");
+if (usingAustralis) {
+  const {
+    ActionButton
+  } = require("sdk/ui/button/action");
+} else {
+  const {
+    Widget
+  } = require("sdk/widget");
+}
+exports.usingAustralis = usingAustralis;
+
+const mainPage = data.url("index.html");
+var uiworker = null;
+
+exports.mainPage = mainPage;
+exports.attachToLightbeamPage = attachToLightbeamPage;
+// These attach page workers to new tabs.
+exports.onForWorker = function (eventname, handler) {
+  if (uiworker) {
+    uiworker.port.on(eventname, handler);
+  } else {
+    console.log('no uiworker to subscript to order');
+  }
+};
+
+exports.emitForWorker = function (eventname, obj) {
+  if (uiworker) {
+    uiworker.port.emit(eventname, obj);
+  }
+};
+
+// Begin tab handlers. These are for sidebar functionality, which is not
+// present yet.
+// FIXME: Move tab handlers into a tab component
+// And specify what we're trying to do with it
+
+function lightbeamTabClosed(tab) {
+  menuitem.label = "Show Lightbeam";
+  button.tooltip = "Show Lightbeam";
+}
+
+function lightbeamTabReady(tab) {
+  menuitem.label = "Close Lightbeam";
+  button.tooltip = "Close Lightbeam";
+}
+
+function lightbeamTabDeactivate(tab) {
+  menuitem.label = "Switch to Lightbeam Tab";
+  button.tooltip = "Switch to Lightbeam Tab";
+}
+
+const blockmap = ss.storage.blockmap;
+var blocksites = Object.keys(blockmap);
+console.log("blocking " + blocksites.length + ' sites');
+
+// This is the heart of the Lightbeam blocking functionality.
+ContentPolicy({
+  description: "Blocks user-defined blocklist from Lightbeam UI",
+  shouldLoad: function ({
+    location: location,
+    origin: origin
+  }) {
+    // ignore URIs with no host
+    var topLevelDomain;
+    try {
+      topLevelDomain = Connection.getDomain(location.host);
+    } catch (e) {
+      // See Issue 374: https://github.com/mozilla/lightbeam/issues/374
+      // if there is no host, like in about:what, then the host getter throws
+      return true;
+    }
+
+    if (blockmap[topLevelDomain]) {
+      return false;
+    }
+    return true;
+  }
+});
+
+function handlePrivateTab(tab) {
+  if (isPrivate(tab) && uiworker) {
+    // console.error('tab is private and uiworker exists');
+    uiworker.port.emit("private-browsing");
+    // console.error('sent message');
+    return true;
+  }
+}
+
+// if there is a private tab opened while the lightbeam tab is open,
+// then alert the user about it.
+tabs.on('open', handlePrivateTab);
+
+// Notify the user in case they open a private window. Connections are
+// visualized but never stored.
+function hasPrivateTab() {
+  // console.error('hasPrivateTab: %s tabs to test', tabs.length);
+  for (var i = 0; i < tabs.length; i++) {
+    if (handlePrivateTab(tabs[i])) {
+      break; // the presence of a Private Window has been detected
+    }
+  }
+}
+
+// Connect the tab to the content script of the UI page. There may only ever be
+// one UI page.
+function attachToLightbeamPage(worker) {
+  console.debug("Attaching to lightbeam page");
+  uiworker = worker;
+
+  // The blocklist is maintained on both sides to reduce latency. However,
+  // this may cause sync errors.
+  function onWorkerUpdateBlocklist(site, blockFlag) {
+    if (blockFlag) {
+      if (!blockmap[site]) {
+        blockmap[site] = true;
+      }
+    } else {
+      if (blockmap[site]) {
+        delete blockmap[site];
+      }
+    }
+
+    uiworker.port.emit('update-blocklist', {
+      domain: site,
+      flag: blockFlag
+    });
+  }
+
+  function onPrefChanged(event) {
+    console.debug("Received updated prefs", JSON.stringify(event));
+    if ("contributeData" in event) {
+      prefs.contributeData = event.contributeData;
+    }
+    if ("defaultVisualization" in event) {
+      prefs.defaultVisualization = event.defaultVisualization;
+    }
+    if ("defaultFilter" in event) {
+      prefs.defaultFilter = event.defaultFilter;
+    }
+  }
+
+  // Send over the the blocklist initially so we can use it.
+  worker.port.emit('update-blocklist-all',
+    Object.keys(blockmap).map(
+      function (site) {
+        return {
+          domain: site,
+          flag: blockmap[site]
+        };
+      }));
+
+  function onWorkerReset() {
+    // Reset buffered state
+    Connection.reset();
+    // And stored state, including prefs
+    persist.reset();
+  }
+
+  function onUIReady() {
+    worker.port.emit("updateUIFromMetadata", { version: self.version });
+    worker.port.emit("updateUIFromPrefs", prefs);
+    worker.port.emit("passStoredConnections", getAllConnections());
+  }
+
+  function onWorkerDetach() {
+    // console.error('detaching lightbeam view');
+    /* jshint validthis:true */
+    this.port.removeListener('reset', onWorkerReset);
+    this.port.removeListener('uiready', onUIReady);
+    this.port.removeListener('updateBlocklist', onWorkerUpdateBlocklist);
+    this.port.removeListener("prefChanged", onPrefChanged);
+    uiworker = null;
+    this.destroy();
+  }
+
+  worker.on("detach", onWorkerDetach);
+  worker.port.on("reset", onWorkerReset);
+  worker.port.on('uiready', onUIReady);
+  worker.port.on('updateBlocklist', onWorkerUpdateBlocklist);
+  worker.port.on("prefChanged", onPrefChanged);
+  worker.port.emit('init');
+
+  // if there is a private window open, then alert the user about it.
+  try {
+    hasPrivateTab();
+  } catch (e) {
+    console.error('Error testing with hasPrivateTab(): %o', e);
+  }
+}
+
+// This lets us toggle between the 3 states (no lightbeam tab open, lightbeam
+// tab open but it's not the tab you're on, you're on the lightbeam tab)
+function getLightbeamTab() {
+  for each(let tab in tabs) {
+    if (tab.url.slice(0, mainPage.length) === mainPage) {
+      return tab;
+    }
+  }
+}
+exports.getLightbeamTab = getLightbeamTab;
+
+// Set up the menu item to open the main UI page:
+var menuitem = require("shared/menuitems").Menuitem({
+  id: "lightbeam_openUITab",
+  menuid: "menu_ToolsPopup",
+  label: "Show Lightbeam",
+  onCommand: function () {
+    openOrSwitchToOrClose();
+  },
+  insertbefore: "sanitizeItem",
+  image: data.url("icons/lightbeam_logo-only_32x32.png")
+});
+
+function openOrSwitchToOrClose() {
+  // Open the Lightbeam tab, if it doesn't exist.
+  var tab = getLightbeamTab();
+  if (!tab) {
+    return tabs.open({
+      url: mainPage,
+      onOpen: lightbeamTabReady,
+      onClose: lightbeamTabClosed,
+      onReady: lightbeamTabReady,
+      onActivate: lightbeamTabReady,
+      onDeactivate: lightbeamTabDeactivate
+    });
+  }
+  // Close it if it's active.
+  if (tab === tabs.activeTab) {
+    tab.close();
+  } else {
+    // Otherwise, switch to the Lightbeam tab
+    tab.activate();
+    tab.window.activate();
+  }
+}
+exports.openOrSwitchToOrClose = openOrSwitchToOrClose;
+
+// Set up the status bar button to open the main UI page:
+var button;
+if (usingAustralis) {
+  console.debug("Using australis");
+  button = ActionButton({
+    id: "lightbeam_Widget",
+    label: "Lightbeam",
+    tooltip: "Show Lightbeam",
+    // Relative to the data directory
+    icon: {
+      "16": "./icons/lightbeam_logo-only_16x16.png",
+      "32": "./icons/lightbeam_logo-only_32x32.png",
+      "48": "./icons/lightbeam_logo-only_48x48.png",
+    },
+    onClick: function () {
+      openOrSwitchToOrClose();
+    }
+  });
+} else {
+  console.debug("Not using australis");
+  button = Widget({
+    id: "lightbeam_Widget",
+    label: "Lightbeam",
+    tooltip: "Show Lightbeam",
+    contentURL: data.url("icons/lightbeam_logo-only_32x32.png"),
+    onClick: function () {
+      openOrSwitchToOrClose();
+    }
+  });
+}

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-mozext/lightbeam.git



More information about the Pkg-mozext-commits mailing list