Commits

Kenneth Love committed 32bf44a

starting on making this a SPA-ish app.

Comments (0)

Files changed (12)

     if not r:
         return redirect(url_for('setup'))
 
-    keys = r.keys()
-    return render_template('index.html', keys=keys)
+    #keys = r.keys()
+    #return render_template('index.html', keys=keys)
+    return render_template('layout.html')
 
 @app.route('/keys')
 def keys():
     else:
         return render_template('no_key.html', key=key)
 
+@app.route('/key/<key>/json')
+def key_json(key):
+    """ JSON info for the key """
+    r = get_redis_connection(session)
+
+    if not r:
+        return redirect(url_for('setup'))
+
+    if r.exists(key):
+        rtype = r.type(key)
+        if rtype == 'hash':
+            output = r.hgetall(key)
+        elif rtype == 'set':
+            output = r.smembers(key)
+        elif rtype == 'zset':
+            output = r.zrange(key, 0, -1, withscores=True)
+        elif rtype == 'list':
+            output = [r.lindex(key, n) for n in xrange(r.llen(key))]
+        else:
+            output = r.get(key)
+        return jsonify(
+            rtype=rtype,
+            key=key,
+            output=output,
+            ttl=r.ttl(key)
+        )
+    else:
+        return jsonify({})
+
+
+
 # Read/write views
 @app.route('/new', methods=['GET', 'POST'])
 def new_key():
     else:
         search_string = '*'
 
-    return jsonify(keys=r.keys(pattern=search_string))
+    return jsonify(
+        keys=[{'key': v} for k,v in enumerate(r.keys(pattern=search_string))])
 
 if __name__ == '__main__':
     app.run()

static/js/LAB.min.js

+/*! LAB.js (LABjs :: Loading And Blocking JavaScript)
+    v1.2.0 (c) Kyle Simpson
+    MIT License
+*/
+(function(p){var q="string",w="head",L="body",M="script",u="readyState",j="preloaddone",x="loadtrigger",N="srcuri",E="preload",Z="complete",y="done",z="which",O="preserve",F="onreadystatechange",ba="onload",P="hasOwnProperty",bb="script/cache",Q="[object ",bw=Q+"Function]",bx=Q+"Array]",e=null,h=true,i=false,k=p.document,bc=p.location,bd=p.ActiveXObject,A=p.setTimeout,be=p.clearTimeout,R=function(a){return k.getElementsByTagName(a)},S=Object.prototype.toString,G=function(){},r={},T={},bf=/^[^?#]*\//.exec(bc.href)[0],bg=/^\w+\:\/\/\/?[^\/]+/.exec(bf)[0],by=R(M),bh=p.opera&&S.call(p.opera)==Q+"Opera]",bi=("MozAppearance"in k.documentElement.style),bj=(k.createElement(M).async===true),v={cache:!(bi||bh),order:bi||bh||bj,xhr:h,dupe:h,base:"",which:w};v[O]=i;v[E]=h;r[w]=k.head||R(w);r[L]=R(L);function B(a){return S.call(a)===bw}function U(a,b){var c=/^\w+\:\/\//,d;if(typeof a!=q)a="";if(typeof b!=q)b="";d=((/^\/\//.test(a))?bc.protocol:"")+a;d=(c.test(d)?"":b)+d;return((c.test(d)?"":(d.charAt(0)==="/"?bg:bf))+d)}function bz(a){return(U(a).indexOf(bg)===0)}function bA(a){var b,c=-1;while(b=by[++c]){if(typeof b.src==q&&a===U(b.src)&&b.type!==bb)return h}return i}function H(t,l){t=!(!t);if(l==e)l=v;var bk=i,C=t&&l[E],bl=C&&l.cache,I=C&&l.order,bm=C&&l.xhr,bB=l[O],bC=l.which,bD=l.base,bn=G,J=i,D,s=h,m={},K=[],V=e;C=bl||bm||I;function bo(a,b){if((a[u]&&a[u]!==Z&&a[u]!=="loaded")||b[y]){return i}a[ba]=a[F]=e;return h}function W(a,b,c){c=!(!c);if(!c&&!(bo(a,b)))return;b[y]=h;for(var d in m){if(m[P](d)&&!(m[d][y]))return}bk=h;bn()}function bp(a){if(B(a[x])){a[x]();a[x]=e}}function bE(a,b){if(!bo(a,b))return;b[j]=h;A(function(){r[b[z]].removeChild(a);bp(b)},0)}function bF(a,b){if(a[u]===4){a[F]=G;b[j]=h;A(function(){bp(b)},0)}}function X(b,c,d,g,f,n){var o=b[z];A(function(){if("item"in r[o]){if(!r[o][0]){A(arguments.callee,25);return}r[o]=r[o][0]}var a=k.createElement(M);if(typeof d==q)a.type=d;if(typeof g==q)a.charset=g;if(B(f)){a[ba]=a[F]=function(){f(a,b)};a.src=c;if(bj){a.async=i}}r[o].insertBefore(a,(o===w?r[o].firstChild:e));if(typeof n==q){a.text=n;W(a,b,h)}},0)}function bq(a,b,c,d){T[a[N]]=h;X(a,b,c,d,W)}function br(a,b,c,d){var g=arguments;if(s&&a[j]==e){a[j]=i;X(a,b,bb,d,bE)}else if(!s&&a[j]!=e&&!a[j]){a[x]=function(){br.apply(e,g)}}else if(!s){bq.apply(e,g)}}function bs(a,b,c,d){var g=arguments,f;if(s&&a[j]==e){a[j]=i;f=a.xhr=(bd?new bd("Microsoft.XMLHTTP"):new p.XMLHttpRequest());f[F]=function(){bF(f,a)};f.open("GET",b);f.send("")}else if(!s&&a[j]!=e&&!a[j]){a[x]=function(){bs.apply(e,g)}}else if(!s){T[a[N]]=h;X(a,b,c,d,e,a.xhr.responseText);a.xhr=e}}function bt(a){if(typeof a=="undefined"||!a)return;if(a.allowDup==e)a.allowDup=l.dupe;var b=a.src,c=a.type,d=a.charset,g=a.allowDup,f=U(b,bD),n,o=bz(f);if(typeof d!=q)d=e;g=!(!g);if(!g&&((T[f]!=e)||(s&&m[f])||bA(f))){if(m[f]!=e&&m[f][j]&&!m[f][y]&&o){W(e,m[f],h)}return}if(m[f]==e)m[f]={};n=m[f];if(n[z]==e)n[z]=bC;n[y]=i;n[N]=f;J=h;if(!I&&bm&&o)bs(n,f,c,d);else if(!I&&bl)br(n,f,c,d);else bq(n,f,c,d)}function Y(a){if(t&&!I)K.push(a);if(!t||C)a()}function bu(a){var b=[],c;for(c=-1;++c<a.length;){if(S.call(a[c])===bx)b=b.concat(bu(a[c]));else b[b.length]=a[c]}return b}D={script:function(){be(V);var a=bu(arguments),b=D,c;if(bB){for(c=-1;++c<a.length;){if(B(a[c]))a[c]=a[c]();if(c===0){Y(function(){bt((typeof a[0]==q)?{src:a[0]}:a[0])})}else b=b.script(a[c]);b=b.wait()}}else{for(c=-1;++c<a.length;){if(B(a[c]))a[c]=a[c]()}Y(function(){for(c=-1;++c<a.length;){bt((typeof a[c]==q)?{src:a[c]}:a[c])}})}V=A(function(){s=i},5);return b},wait:function(a){be(V);s=i;if(!B(a))a=G;var b=H(t||J,l),c=b.trigger,d=function(){try{a()}catch(err){}c()};delete b.trigger;var g=function(){if(J&&!bk)bn=d;else d()};if(t&&!J)K.push(g);else Y(g);return b}};if(t){D.trigger=function(){var a,b=-1;while(a=K[++b])a();K=[]}}else D.trigger=G;return D}function bv(a){var b,c={},d={"UseCachePreload":"cache","UseLocalXHR":"xhr","UsePreloading":E,"AlwaysPreserveOrder":O,"AllowDuplicates":"dupe"},g={"AppendTo":z,"BasePath":"base"};for(b in d)g[b]=d[b];c.order=!(!v.order);for(b in g){if(g[P](b)&&v[g[b]]!=e)c[g[b]]=(a[b]!=e)?a[b]:v[g[b]]}for(b in d){if(d[P](b))c[d[b]]=!(!c[d[b]])}if(!c[E])c.cache=c.order=c.xhr=i;c.which=(c.which===w||c.which===L)?c.which:w;return c}p.$LAB={setGlobalDefaults:function(a){v=bv(a)},setOptions:function(a){return H(i,bv(a))},script:function(){return H().script.apply(e,arguments)},wait:function(){return H().wait.apply(e,arguments)}};(function(a,b,c){if(k[u]==e&&k[a]){k[u]="loading";k[a](b,c=function(){k.removeEventListener(b,c,i);k[u]=Z},i)}})("addEventListener","DOMContentLoaded")})(window);

static/js/backbone-localstorage.js

+// A simple module to replace `Backbone.sync` with *localStorage*-based
+// persistence. Models are given GUIDS, and saved into a JSON object. Simple
+// as that.
+
+// Generate four random hex digits.
+function S4() {
+   return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
+};
+
+// Generate a pseudo-GUID by concatenating random hexadecimal.
+function guid() {
+   return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
+};
+
+// Our Store is represented by a single JS object in *localStorage*. Create it
+// with a meaningful name, like the name you'd give a table.
+var Store = function(name) {
+  this.name = name;
+  var store = localStorage.getItem(this.name);
+  this.data = (store && JSON.parse(store)) || {};
+};
+
+_.extend(Store.prototype, {
+
+  // Save the current state of the **Store** to *localStorage*.
+  save: function() {
+    localStorage.setItem(this.name, JSON.stringify(this.data));
+  },
+
+  // Add a model, giving it a (hopefully)-unique GUID, if it doesn't already
+  // have an id of it's own.
+  create: function(model) {
+    if (!model.id) model.id = model.attributes.id = guid();
+    this.data[model.id] = model;
+    this.save();
+    return model;
+  },
+
+  // Update a model by replacing its copy in `this.data`.
+  update: function(model) {
+    this.data[model.id] = model;
+    this.save();
+    return model;
+  },
+
+  // Retrieve a model from `this.data` by id.
+  find: function(model) {
+    return this.data[model.id];
+  },
+
+  // Return the array of all models currently in storage.
+  findAll: function() {
+    return _.values(this.data);
+  },
+
+  // Delete a model from `this.data`, returning it.
+  destroy: function(model) {
+    delete this.data[model.id];
+    this.save();
+    return model;
+  }
+
+});
+
+// Override `Backbone.sync` to use delegate to the model or collection's
+// *localStorage* property, which should be an instance of `Store`.
+Backbone.sync = function(method, model, success, error) {
+
+  var resp;
+  var store = model.localStorage || model.collection.localStorage;
+
+  switch (method) {
+    case "read":    resp = model.id ? store.find(model) : store.findAll(); break;
+    case "create":  resp = store.create(model);                            break;
+    case "update":  resp = store.update(model);                            break;
+    case "delete":  resp = store.destroy(model);                           break;
+  }
+
+  if (resp) {
+    success(resp);
+  } else {
+    error("Record not found");
+  }
+};

static/js/backbone-min.js

+// Backbone.js 0.3.3
+// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
+// Backbone may be freely distributed under the MIT license.
+// For all details and documentation:
+// http://documentcloud.github.com/backbone
+(function(){var e;e=typeof exports!=="undefined"?exports:this.Backbone={};e.VERSION="0.3.3";var f=this._;if(!f&&typeof require!=="undefined")f=require("underscore")._;var h=this.jQuery||this.Zepto;e.emulateHTTP=false;e.emulateJSON=false;e.Events={bind:function(a,b){this._callbacks||(this._callbacks={});(this._callbacks[a]||(this._callbacks[a]=[])).push(b);return this},unbind:function(a,b){var c;if(a){if(c=this._callbacks)if(b){c=c[a];if(!c)return this;for(var d=0,g=c.length;d<g;d++)if(b===c[d]){c.splice(d,
+1);break}}else c[a]=[]}else this._callbacks={};return this},trigger:function(a){var b,c,d,g;if(!(c=this._callbacks))return this;if(b=c[a]){d=0;for(g=b.length;d<g;d++)b[d].apply(this,Array.prototype.slice.call(arguments,1))}if(b=c.all){d=0;for(g=b.length;d<g;d++)b[d].apply(this,arguments)}return this}};e.Model=function(a,b){a||(a={});if(this.defaults)a=f.extend({},this.defaults,a);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.set(a,{silent:true});this._previousAttributes=
+f.clone(this.attributes);if(b&&b.collection)this.collection=b.collection;this.initialize(a,b)};f.extend(e.Model.prototype,e.Events,{_previousAttributes:null,_changed:false,initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.attributes[a];return this._escapedAttributes[a]=(b==null?"":b).replace(/&(?!\w+;)/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,
+"&quot;")},set:function(a,b){b||(b={});if(!a)return this;if(a.attributes)a=a.attributes;var c=this.attributes,d=this._escapedAttributes;if(!b.silent&&this.validate&&!this._performValidation(a,b))return false;if("id"in a)this.id=a.id;for(var g in a){var i=a[g];if(!f.isEqual(c[g],i)){c[g]=i;delete d[g];if(!b.silent){this._changed=true;this.trigger("change:"+g,this,i,b)}}}!b.silent&&this._changed&&this.change(b);return this},unset:function(a,b){b||(b={});var c={};c[a]=void 0;if(!b.silent&&this.validate&&
+!this._performValidation(c,b))return false;delete this.attributes[a];delete this._escapedAttributes[a];if(!b.silent){this._changed=true;this.trigger("change:"+a,this,void 0,b);this.change(b)}return this},clear:function(a){a||(a={});var b=this.attributes,c={};for(attr in b)c[attr]=void 0;if(!a.silent&&this.validate&&!this._performValidation(c,a))return false;this.attributes={};this._escapedAttributes={};if(!a.silent){this._changed=true;for(attr in b)this.trigger("change:"+attr,this,void 0,a);this.change(a)}return this},
+fetch:function(a){a||(a={});var b=this,c=j(a.error,b,a);(this.sync||e.sync)("read",this,function(d){if(!b.set(b.parse(d),a))return false;a.success&&a.success(b,d)},c);return this},save:function(a,b){b||(b={});if(a&&!this.set(a,b))return false;var c=this,d=j(b.error,c,b),g=this.isNew()?"create":"update";(this.sync||e.sync)(g,this,function(i){if(!c.set(c.parse(i),b))return false;b.success&&b.success(c,i)},d);return this},destroy:function(a){a||(a={});var b=this,c=j(a.error,b,a);(this.sync||e.sync)("delete",
+this,function(d){b.collection&&b.collection.remove(b);a.success&&a.success(b,d)},c);return this},url:function(){var a=k(this.collection);if(this.isNew())return a;return a+(a.charAt(a.length-1)=="/"?"":"/")+this.id},parse:function(a){return a},clone:function(){return new this.constructor(this)},isNew:function(){return!this.id},change:function(a){this.trigger("change",this,a);this._previousAttributes=f.clone(this.attributes);this._changed=false},hasChanged:function(a){if(a)return this._previousAttributes[a]!=
+this.attributes[a];return this._changed},changedAttributes:function(a){a||(a=this.attributes);var b=this._previousAttributes,c=false,d;for(d in a)if(!f.isEqual(b[d],a[d])){c=c||{};c[d]=a[d]}return c},previous:function(a){if(!a||!this._previousAttributes)return null;return this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},_performValidation:function(a,b){var c=this.validate(a);if(c){b.error?b.error(this,c):this.trigger("error",this,c,b);return false}return true}});
+e.Collection=function(a,b){b||(b={});if(b.comparator){this.comparator=b.comparator;delete b.comparator}this._boundOnModelEvent=f.bind(this._onModelEvent,this);this._reset();a&&this.refresh(a,{silent:true});this.initialize(a,b)};f.extend(e.Collection.prototype,e.Events,{model:e.Model,initialize:function(){},toJSON:function(){return this.map(function(a){return a.toJSON()})},add:function(a,b){if(f.isArray(a))for(var c=0,d=a.length;c<d;c++)this._add(a[c],b);else this._add(a,b);return this},remove:function(a,
+b){if(f.isArray(a))for(var c=0,d=a.length;c<d;c++)this._remove(a[c],b);else this._remove(a,b);return this},get:function(a){if(a==null)return null;return this._byId[a.id!=null?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");this.models=this.sortBy(this.comparator);a.silent||this.trigger("refresh",this,a);return this},pluck:function(a){return f.map(this.models,
+function(b){return b.get(a)})},refresh:function(a,b){a||(a=[]);b||(b={});this._reset();this.add(a,{silent:true});b.silent||this.trigger("refresh",this,b);return this},fetch:function(a){a||(a={});var b=this,c=j(a.error,b,a);(this.sync||e.sync)("read",this,function(d){b.refresh(b.parse(d));a.success&&a.success(b,d)},c);return this},create:function(a,b){var c=this;b||(b={});if(a instanceof e.Model)a.collection=c;else a=new this.model(a,{collection:c});return a.save(null,{success:function(d,g){c.add(d);
+b.success&&b.success(d,g)},error:b.error})},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId={};this._byCid={}},_add:function(a,b){b||(b={});a instanceof e.Model||(a=new this.model(a,{collection:this}));var c=this.getByCid(a);if(c)throw Error(["Can't add the same model to a set twice",c.id]);this._byId[a.id]=a;this._byCid[a.cid]=a;a.collection=this;this.models.splice(this.comparator?this.sortedIndex(a,this.comparator):
+this.length,0,a);a.bind("all",this._boundOnModelEvent);this.length++;b.silent||a.trigger("add",a,this,b);return a},_remove:function(a,b){b||(b={});a=this.getByCid(a)||this.get(a);if(!a)return null;delete this._byId[a.id];delete this._byCid[a.cid];delete a.collection;this.models.splice(this.indexOf(a),1);this.length--;b.silent||a.trigger("remove",a,this,b);a.unbind("all",this._boundOnModelEvent);return a},_onModelEvent:function(a,b){if(a==="change:id"){delete this._byId[b.previous("id")];this._byId[b.id]=
+b}this.trigger.apply(this,arguments)}});f.each(["forEach","each","map","reduce","reduceRight","find","detect","filter","select","reject","every","all","some","any","include","invoke","max","min","sortBy","sortedIndex","toArray","size","first","rest","last","without","indexOf","lastIndexOf","isEmpty"],function(a){e.Collection.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});e.Controller=function(a){a||(a={});if(a.routes)this.routes=a.routes;this._bindRoutes();
+this.initialize(a)};var o=/:([\w\d]+)/g,p=/\*([\w\d]+)/g;f.extend(e.Controller.prototype,e.Events,{initialize:function(){},route:function(a,b,c){e.history||(e.history=new e.History);f.isRegExp(a)||(a=this._routeToRegExp(a));e.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d))},this))},saveLocation:function(a){e.history.saveLocation(a)},_bindRoutes:function(){if(this.routes)for(var a in this.routes){var b=this.routes[a];
+this.route(a,b,this[b])}},_routeToRegExp:function(a){a=a.replace(o,"([^/]*)").replace(p,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});e.History=function(){this.handlers=[];this.fragment=this.getFragment();f.bindAll(this,"checkUrl")};var l=/^#*/;f.extend(e.History.prototype,{interval:50,getFragment:function(a){return(a||window.location).hash.replace(l,"")},start:function(){var a=document.documentMode;if(a=h.browser.msie&&(!a||a<=7))this.iframe=h('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow;
+"onhashchange"in window&&!a?h(window).bind("hashchange",this.checkUrl):setInterval(this.checkUrl,this.interval);return this.loadUrl()},route:function(a,b){this.handlers.push({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();if(a==this.fragment&&this.iframe)a=this.getFragment(this.iframe.location);if(a==this.fragment||a==decodeURIComponent(this.fragment))return false;if(this.iframe)window.location.hash=this.iframe.location.hash=a;this.loadUrl()},loadUrl:function(){var a=this.fragment=
+this.getFragment();return f.any(this.handlers,function(b){if(b.route.test(a)){b.callback(a);return true}})},saveLocation:function(a){a=(a||"").replace(l,"");if(this.fragment!=a){window.location.hash=this.fragment=a;if(this.iframe&&a!=this.getFragment(this.iframe.location)){this.iframe.document.open().close();this.iframe.location.hash=a}}}});e.View=function(a){this._configure(a||{});this._ensureElement();this.delegateEvents();this.initialize(a)};var q=/^(\w+)\s*(.*)$/;f.extend(e.View.prototype,e.Events,
+{tagName:"div",$:function(a){return h(a,this.el)},initialize:function(){},render:function(){return this},remove:function(){h(this.el).remove();return this},make:function(a,b,c){a=document.createElement(a);b&&h(a).attr(b);c&&h(a).html(c);return a},delegateEvents:function(a){if(a||(a=this.events)){h(this.el).unbind();for(var b in a){var c=a[b],d=b.match(q),g=d[1];d=d[2];c=f.bind(this[c],this);d===""?h(this.el).bind(g,c):h(this.el).delegate(d,g,c)}}},_configure:function(a){if(this.options)a=f.extend({},
+this.options,a);if(a.model)this.model=a.model;if(a.collection)this.collection=a.collection;if(a.el)this.el=a.el;if(a.id)this.id=a.id;if(a.className)this.className=a.className;if(a.tagName)this.tagName=a.tagName;this.options=a},_ensureElement:function(){if(!this.el){var a={};if(this.id)a.id=this.id;if(this.className)a["class"]=this.className;this.el=this.make(this.tagName,a)}}});var m=function(a,b){var c=r(this,a,b);c.extend=m;return c};e.Model.extend=e.Collection.extend=e.Controller.extend=e.View.extend=
+m;var s={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};e.sync=function(a,b,c,d){var g=s[a];a=a==="create"||a==="update"?JSON.stringify(b.toJSON()):null;b={url:k(b),type:g,contentType:"application/json",data:a,dataType:"json",processData:false,success:c,error:d};if(e.emulateJSON){b.contentType="application/x-www-form-urlencoded";b.processData=true;b.data=a?{model:a}:{}}if(e.emulateHTTP)if(g==="PUT"||g==="DELETE"){if(e.emulateJSON)b.data._method=g;b.type="POST";b.beforeSend=function(i){i.setRequestHeader("X-HTTP-Method-Override",
+g)}}h.ajax(b)};var n=function(){},r=function(a,b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){return a.apply(this,arguments)};n.prototype=a.prototype;d.prototype=new n;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},k=function(a){if(!(a&&a.url))throw Error("A 'url' property or function must be specified");return f.isFunction(a.url)?a.url():a.url},j=function(a,b,c){return function(d){a?a(b,d):b.trigger("error",b,d,c)}}})();

static/js/cacheprovider.js

+/*
 * Dustin Diaz's CacheProvider
 * From: http://www.dustindiaz.com/javascript-cache-provider/
  // values will be stored here
  this._cache = {};
  /**
     * {String} k - the key
     * {Boolean} local - get this from local storage?
     * {Boolean} o - is the value you put in local storage an object?
     */
  get: function(k, local, o) {
    if (local && CacheProvider.hasLocalStorage) {
      var action = o ? 'getObject' : 'getItem';
      return localStorage[action](k) || undefined;
    } else {
      return this._cache[k] || undefined;
    }
  },
  /**
     * {String} k - the key
     * {Object} v - any kind of value you want to store
     * however only objects and strings are allowed in local storage
     * {Boolean} local - put this in local storage
     */
  set: function(k, v, local) {
  
  CacheProvider.hasLocalStorage = ('localStorage' in window) && window['localStorage'] !== null && typeof Storage !== 'undefined';
  
    if (local && CacheProvider.hasLocalStorage) {
      if (typeof v !== 'string') {
        // make assumption if it's not a string, then we're storing an object
        localStorage.setObject(k, v);
      } else {
        try {
          localStorage.setItem(k, v);
        } catch (ex) {
          if (ex.name == 'QUOTA_EXCEEDED_ERR') {
            // developer needs to figure out what to start invalidating
            throw new Exception(v);
            return;
          }
        }
      }
    } else {
      // put in our local object
      this._cache[k] = v;
    }
    // return our newly cached item
    return v;
  },
  /**
     * {String} k - the key
     * {Boolean} local - put this in local storage
     * {Boolean} o - is this an object you want to put in local storage?
     */
  clear: function(k, local, o) {
    if (local && CacheProvider.hasLocalStorage) {
      localStorage.removeItem(k);
    }
    // delete in both caches - doesn't hurt.
    delete this._cache[k];
  }

static/js/fLAB.min.js

+// fLAB.js (file:// protocol adapter for LABjs 1.0+) | v0.2 (c) Kyle Simpson | MIT License
+(function(m){var t=m.$LAB,u=m.document,C=u.location,L=(C.protocol==="file:");if(!t||!L)return;var h="undefined",j="string",o="head",v="body",M="function",D="script",E="srcuri",N="done",p="which",w=true,k=false,F=m.setTimeout,x=function(b){return u.getElementsByTagName(b)},O=Object.prototype.toString,G=function(){},q={},y={},H=/^[^?#]*\//.exec(C.href)[0],P=/^file:\/\/(localhost)?(\/[a-z]:)?/i.exec(H)[0],Q=x(D),R=!+"\v1",I=R,r={dupe:k,preserve:k,base:"",which:o};q[o]=x(o);q[v]=x(v);function s(b,c){if(typeof b!==j)b="";if(typeof c!==j)c="";var a=(/^file\:\/\//.test(b)?"":c)+b;return((/^file\:\/\//.test(a)?"":(a.charAt(0)==="/"?P:H))+a)}function S(b){var c=0,a;while(a=Q[c++]){if(typeof a.src===j&&b===s(a.src))return w}return k}function z(l){if(typeof l===h)l=r;var Y=k,T=l.which,U=l.base,Z=G,V=k,n,A={},i=null;function W(a,e,d,f){if(q[a[p]][0]===null){F(arguments.callee,25);return}var g=u.createElement(D),B=function(b,c){g.setAttribute(b,c)};B("type",d);if(typeof f===j)B("charset",f);B("src",e);q[a[p]][0].appendChild(g)}function X(b){if(typeof b.allowDup===h)b.allowDup=l.dupe;var c=b.src,a=b.type,e=b.charset,d=b.allowDup,f=s(c,U),g;if(typeof a!==j)a="text/javascript";if(typeof e!==j)e=null;d=!(!d);if(!d&&((typeof y[f]!==h&&y[f]!==null)||S(f))){return}if(typeof A[f]===h)A[f]={};g=A[f];if(typeof g[p]===h)g[p]=T;g[N]=k;g[E]=f;V=w;y[g[E]]=w;W(g,f,a,e)}function J(b){var c=[],a;for(a=0;a<b.length;a++){if(O.call(b[a])==="[object Array]")c=c.concat(J(b[a]));else c[c.length]=b[a]}return c}n={script:function(){var b=J(arguments),c,a;for(a=0;a<b.length;a++){if(typeof b[a]===j)b[a]={src:s(b[a])};else if(typeof b[a].src!==h)b[a].src=s(b[a].src)}if(I){c=n;for(a=0;a<b.length;a++)X(b[a])}else{if(i===null)i=t.setOptions(l.pubMap);c=i=i.script.apply(null,b)}return c},wait:function(b){var c;if(typeof b!==M)b=G;if(I){c=n;F(b,0)}else{if(i===null)i=t.setOptions(l.pubMap);c=i=i.wait(b)}return c}};n.block=n.wait;return n}function K(b){var c,a={},e={"AlwaysPreserveOrder":"preserve","AllowDuplicates":"dupe"},d={"AppendTo":"which","BasePath":"base"};for(c in e)d[c]=e[c];for(c in d){if(d.hasOwnProperty(c)&&typeof r[d[c]]!==h)a[d[c]]=(b&&typeof b[c]!==h)?b[c]:r[d[c]]}for(c in e){if(e.hasOwnProperty(c))a[e[c]]=!(!a[e[c]])}a.preload=a.cache=a.order=a.xhr=k;a.which=(a.which===o||a.which===v)?a.which:o;a.pubMap={};for(c in d){if(d.hasOwnProperty(c))a.pubMap[c]=a[d[c]]}return a}m.$LAB={setGlobalDefaults:function(b){r=K(b)},setOptions:function(b){return z(K(b))},script:function(){return z().script.apply(null,arguments)},wait:function(){return z().wait.apply(null,arguments)}};m.$LAB.block=m.$LAB.wait})(window);

static/js/jquery.tmpl.min.js

+/*
+ * jQuery Templates Plugin 1.0.0pre
+ * http://github.com/jquery/jquery-tmpl
+ * Requires jQuery 1.4.2
+ *
+ * Copyright Software Freedom Conservancy, Inc.
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ */
+(function(a){var r=a.fn.domManip,d="_tmplitem",q=/^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,b={},f={},e,p={key:0,data:{}},i=0,c=0,l=[];function g(g,d,h,e){var c={data:e||(e===0||e===false)?e:d?d.data:{},_wrap:d?d._wrap:null,tmpl:null,parent:d||null,nodes:[],calls:u,nest:w,wrap:x,html:v,update:t};g&&a.extend(c,g,{nodes:[],parent:d});if(h){c.tmpl=h;c._ctnt=c._ctnt||c.tmpl(a,c);c.key=++i;(l.length?f:b)[i]=c}return c}a.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(f,d){a.fn[f]=function(n){var g=[],i=a(n),k,h,m,l,j=this.length===1&&this[0].parentNode;e=b||{};if(j&&j.nodeType===11&&j.childNodes.length===1&&i.length===1){i[d](this[0]);g=this}else{for(h=0,m=i.length;h<m;h++){c=h;k=(h>0?this.clone(true):this).get();a(i[h])[d](k);g=g.concat(k)}c=0;g=this.pushStack(g,f,i.selector)}l=e;e=null;a.tmpl.complete(l);return g}});a.fn.extend({tmpl:function(d,c,b){return a.tmpl(this[0],d,c,b)},tmplItem:function(){return a.tmplItem(this[0])},template:function(b){return a.template(b,this[0])},domManip:function(d,m,k){if(d[0]&&a.isArray(d[0])){var g=a.makeArray(arguments),h=d[0],j=h.length,i=0,f;while(i<j&&!(f=a.data(h[i++],"tmplItem")));if(f&&c)g[2]=function(b){a.tmpl.afterManip(this,b,k)};r.apply(this,g)}else r.apply(this,arguments);c=0;!e&&a.tmpl.complete(b);return this}});a.extend({tmpl:function(d,h,e,c){var i,k=!c;if(k){c=p;d=a.template[d]||a.template(null,d);f={}}else if(!d){d=c.tmpl;b[c.key]=c;c.nodes=[];c.wrapped&&n(c,c.wrapped);return a(j(c,null,c.tmpl(a,c)))}if(!d)return[];if(typeof h==="function")h=h.call(c||{});e&&e.wrapped&&n(e,e.wrapped);i=a.isArray(h)?a.map(h,function(a){return a?g(e,c,d,a):null}):[g(e,c,d,h)];return k?a(j(c,null,i)):i},tmplItem:function(b){var c;if(b instanceof a)b=b[0];while(b&&b.nodeType===1&&!(c=a.data(b,"tmplItem"))&&(b=b.parentNode));return c||p},template:function(c,b){if(b){if(typeof b==="string")b=o(b);else if(b instanceof a)b=b[0]||{};if(b.nodeType)b=a.data(b,"tmpl")||a.data(b,"tmpl",o(b.innerHTML));return typeof c==="string"?(a.template[c]=b):b}return c?typeof c!=="string"?a.template(null,c):a.template[c]||a.template(null,q.test(c)?c:a(c)):null},encode:function(a){return(""+a).split("<").join("&lt;").split(">").join("&gt;").split('"').join("&#34;").split("'").join("&#39;")}});a.extend(a.tmpl,{tag:{tmpl:{_default:{$2:"null"},open:"if($notnull_1){__=__.concat($item.nest($1,$2));}"},wrap:{_default:{$2:"null"},open:"$item.calls(__,$1,$2);__=[];",close:"call=$item.calls();__=call._.concat($item.wrap(call,__));"},each:{_default:{$2:"$index, $value"},open:"if($notnull_1){$.each($1a,function($2){with(this){",close:"}});}"},"if":{open:"if(($notnull_1) && $1a){",close:"}"},"else":{_default:{$1:"true"},open:"}else if(($notnull_1) && $1a){"},html:{open:"if($notnull_1){__.push($1a);}"},"=":{_default:{$1:"$data"},open:"if($notnull_1){__.push($.encode($1a));}"},"!":{open:""}},complete:function(){b={}},afterManip:function(f,b,d){var e=b.nodeType===11?a.makeArray(b.childNodes):b.nodeType===1?[b]:[];d.call(f,b);m(e);c++}});function j(e,g,f){var b,c=f?a.map(f,function(a){return typeof a==="string"?e.key?a.replace(/(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g,"$1 "+d+'="'+e.key+'" $2'):a:j(a,e,a._ctnt)}):e;if(g)return c;c=c.join("");c.replace(/^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/,function(f,c,e,d){b=a(e).get();m(b);if(c)b=k(c).concat(b);if(d)b=b.concat(k(d))});return b?b:k(c)}function k(c){var b=document.createElement("div");b.innerHTML=c;return a.makeArray(b.childNodes)}function o(b){return new Function("jQuery","$item","var $=jQuery,call,__=[],$data=$item.data;with($data){__.push('"+a.trim(b).replace(/([\\'])/g,"\\$1").replace(/[\r\t\n]/g," ").replace(/\$\{([^\}]*)\}/g,"{{= $1}}").replace(/\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,function(m,l,k,g,b,c,d){var j=a.tmpl.tag[k],i,e,f;if(!j)throw"Unknown template tag: "+k;i=j._default||[];if(c&&!/\w$/.test(b)){b+=c;c=""}if(b){b=h(b);d=d?","+h(d)+")":c?")":"";e=c?b.indexOf(".")>-1?b+h(c):"("+b+").call($item"+d:b;f=c?e:"(typeof("+b+")==='function'?("+b+").call($item):("+b+"))"}else f=e=i.$1||"null";g=h(g);return"');"+j[l?"close":"open"].split("$notnull_1").join(b?"typeof("+b+")!=='undefined' && ("+b+")!=null":"true").split("$1a").join(f).split("$1").join(e).split("$2").join(g||i.$2||"")+"__.push('"})+"');}return __;")}function n(c,b){c._wrap=j(c,true,a.isArray(b)?b:[q.test(b)?b:a(b).html()]).join("")}function h(a){return a?a.replace(/\\'/g,"'").replace(/\\\\/g,"\\"):null}function s(b){var a=document.createElement("div");a.appendChild(b.cloneNode(true));return a.innerHTML}function m(o){var n="_"+c,k,j,l={},e,p,h;for(e=0,p=o.length;e<p;e++){if((k=o[e]).nodeType!==1)continue;j=k.getElementsByTagName("*");for(h=j.length-1;h>=0;h--)m(j[h]);m(k)}function m(j){var p,h=j,k,e,m;if(m=j.getAttribute(d)){while(h.parentNode&&(h=h.parentNode).nodeType===1&&!(p=h.getAttribute(d)));if(p!==m){h=h.parentNode?h.nodeType===11?0:h.getAttribute(d)||0:0;if(!(e=b[m])){e=f[m];e=g(e,b[h]||f[h]);e.key=++i;b[i]=e}c&&o(m)}j.removeAttribute(d)}else if(c&&(e=a.data(j,"tmplItem"))){o(e.key);b[e.key]=e;h=a.data(j.parentNode,"tmplItem");h=h?h.key:0}if(e){k=e;while(k&&k.key!=h){k.nodes.push(j);k=k.parent}delete e._ctnt;delete e._wrap;a.data(j,"tmplItem",e)}function o(a){a=a+n;e=l[a]=l[a]||g(e,b[e.parent.key+n]||e.parent)}}}function u(a,d,c,b){if(!a)return l.pop();l.push({_:a,tmpl:d,item:this,data:c,options:b})}function w(d,c,b){return a.tmpl(a.template(d),c,b,this)}function x(b,d){var c=b.options||{};c.wrapped=d;return a.tmpl(a.template(b.tmpl),b.data,c,b.item)}function v(d,c){var b=this._wrap;return a.map(a(a.isArray(b)?b.join(""):b).filter(d||"*"),function(a){return c?a.innerText||a.textContent:a.outerHTML||s(a)})}function t(){var b=this.nodes;a.tmpl(null,null,null,this).insertBefore(b[0]);a(b).remove()}})(jQuery);

static/js/jquery.validjson.js

+/*
+    Copyright © 2008
+    Rob Manson <roBman@MobileOnlineBusiness.com.au>, 
+    Sean McCarthy <sean@MobileOnlineBusiness.com.au> 
+    and http://SOAPjr.org
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+ 
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+    Acknoweldgements
+    ----------------
+    This jQuery plugin utilises and requires the JSONSchema Validator
+    which is created by and Copyright (c) 2007 Kris Zyp SitePen (www.sitepen.com)
+    Licensed under the MIT (MIT-LICENSE.txt) licence.
+
+    JSONSchema Validator - Validates JavaScript objects using JSON Schemas 
+    - http://jsonschema.googlecode.com/files/jsonschema-b2.js
+      (but check for the latest release)
+
+    For more information visit:
+    - http://www.json.com/json-schema-proposal/
+    - http://code.google.com/p/jsonschema/
+
+
+
+    Revision history
+    ----------------
+    v1.0.1  - 4 Dec 2008
+    - Extended $.getValidJSON(params) API so you can define 
+      { schema : { send : xxx, receive : zzz } } so you can 
+      optionally validate data on the way out and on the way in.
+    - Added tests to ensure that the schema key defined does really 
+      exist as a typeof "object" in validJSONSchemas.
+
+    v1.0.0  - 3 Dec 2008
+    - initial release
+
+
+    Support
+    -------
+    For support, suggestions or general discussion please visit the #SOAPjr irc
+    channel on freenode.net or visit http://SOAPjr.org 
+
+
+    Implementation overview
+    -----------------------
+    NOTE:   This work contributes to the SOAPjr Data Model Definition project
+            that is designed to encourage the adoption and use of common JSON
+            data models with common SOAPjr APIs.
+
+            See http://soapjr.org/specs.html#dmd for more information
+
+    1.  Include the jquery.js, jsonschema-b2.js and jquery.ValidJSON.js src files
+        into your html page - optionally include the jquery.SOAPjr.js plugin too
+
+    2.  Add a call to the $(document).ready(function() { ... }) block to load
+        the relevant JSON Schema definition files you require
+
+        e.g.
+                - http://soapjr.org/schemas/SOAPjr_request
+                - see http://json-schema.org/shared.html for common formats
+
+    3.  Get the JSON files/objects you require using the $.getValidJSON() API
+
+    4.  Check the "result.valid" boolean and "result.errors" array for any
+        validation errors and if all is good then use "json" like normal.
+
+    5.  Sleep well knowing your data is cleanly validated and conforms to your
+        chosen schema.
+
+
+    Code usage examples
+    -------------------
+    1. Include src files
+
+        <script type="text/javascript" src="jquery-1.2.6.js"></script>
+        <script type="text/javascript" src="jsonschema-b2.js"></script>
+        <script type="text/javascript" src="jquery.ValidJSON.js"></script>
+
+
+    2. Add calls to load schema files
+
+        <script type="text/javascript">
+        $(document).ready(function() {
+            //load them from a URL
+            $.getValidJSONSchemas({
+                //id          //url
+                "card"      : "http://json-schema.org/card",
+                "calendar"  : "http://json-schema.org/calendar"
+            });
+
+            //import them from a local var
+            $.setValidJSONSchemas({
+                //id          //pre-populated vars (possibly included in .js files)
+                "geo"       : geoschema_var,
+                "address"   : addressschema_var
+            });
+        });
+        </script> 
+
+        NOTE: cross-domain xhr restrictions apply - look at JSONP for cross domain requests
+
+
+    3. Get the JSON files/objects
+
+        <script type="text/javascript">
+        function do_your_stuff() {
+            //collect data from a form or setup a stub
+            var params = { ... };
+
+            //get your data and validate it in one call
+            var xhr = $.getValidJSON({
+                "url"           : url,                                              //required
+                "data"          : params,                                           //optional
+                "callback"      : "my_callback",                                    //required (see point 4 below)
+                "schema"        : { "send":"schema_id1", "receive":"schema_id2" }   //optional - can be defined within the response JSON object using $schema
+            });
+        }
+        </script>
+
+
+    4. Check the validation results/errors
+
+        <script type="text/javascript">
+        //setup your callback to handle the json data and any validation errors
+        function my_callback(result, json) {
+            if (result.valid) {
+                //json contains a valid XXX data object
+            } else {
+                for (var e in result.errors) {
+                    //e.property tells you which value is invalid
+                    //e.message is a human readable error message
+                }
+            }
+        }
+        </script>
+
+
+    5. Relax
+        
+        This plugin will let you do field level schema based validation.
+        You can also connect this to your form validation for standardised form data.
+        And you can use it in combination with the SOAPjr plugin to make sure
+        your apps handle metadata and complex/multiple errors seamlessly.
+
+        Isn't technology great! 8)
+
+*/
+
+//very weak example location script 8) for instant gratification
+//javascript:function f(j, v) { alert(v.valid+" : "+v.errors[0].message) };$.getValidJSON({ "url":"test.json", "data":{"longitude":333, "latitude":123},"callback":"f", "schema": {"send":"geothingy","receive":"geothingy"}});void(0)
+
+var validJSONSchemas = {};
+
+// Example simple JSON Schema - see http://json-schema.org/geo for latest version
+var  geoschema =  {
+    "description":"A geographical coordinate",
+    "type":"object",
+    "properties":{
+        "latitude":{"type":"number"},
+        "longitude":{"type":"number"}
+    }
+};
+
+
+jQuery.setValidJSONSchemas = function(input) {
+    for (var id in input) {
+        if (typeof input[id] == "object") {
+            validJSONSchemas[id] = input[id];
+        }
+    }
+}
+
+jQuery.getValidJSONSchemas = function(input) {
+    for (var id in input) {
+        var url         = input[id];
+        if (!url) {
+            throw("Not even a URL or filename was supplied!");
+        }
+        var xhr = $.ajax({
+            "url"       : url,
+            "complete"  : gotValidJSONSchema
+        });
+        xhr.getValidJSONSchemasConfig = {
+            "id"    : id,
+            "url"   : input[id]
+        };
+    }
+}
+
+function gotValidJSONSchema(xhr, status) {
+    var json = eval("("+xhr.responseText+")");
+    if (xhr.getValidJSONSchemasConfig != null) {
+        if (xhr.getValidJSONSchemasConfig.id) {
+            //this assumes one schema per json returned
+            var id = xhr.getValidJSONSchemasConfig.id;
+            var tmp = {};
+            tmp[id] = json;
+            $.setValidJSONSchemas(tmp);
+        }
+    } else {
+        throw("No config data available for this schema");
+    }
+}
+
+jQuery.getValidJSON = function(input) {  //url, data, callback and schema
+    //TODO: add a queue for xhr's
+    //var req         = new Date().valueOf();
+
+    var url         = input.url;
+    if (!url) {
+        throw("Not even a URL or filename was supplied!");
+    }
+    var data        = input.data || null;
+    var callback    = input.callback;
+    if (!callback) {
+        throw("No callback provided");
+    }
+    var schema      = input.schema || null;
+
+    var config      = { 
+        "callback"  : callback,
+        "schema"    : schema
+    }
+
+    if (schema.send && data) {
+        if (typeof validJSONSchemas[schema.send] == "object") {
+            var result = JSONSchema.validate(data, validJSONSchemas[schema.send]);
+            if (!result.valid) {
+                var errors = [];
+                for (var e in result.errors) {
+                    var tmp = result.errors[e].property+" : "+result.errors[e].message;
+                    errors.push(tmp);
+                }
+                throw(errors.join(", \n"));
+            }
+        } else {
+            throw("Invalid send schema defined - "+schema.send);
+        }
+    }
+
+    if (schema.receive && typeof validJSONSchemas[schema.receive] != "object") {
+        throw("Invalid receive schema defined - "+schema.receive);
+    }
+
+    var ajax_options = {
+        "url"       : url, 
+        "complete"  : gotValidJSON,
+    };
+    if (data) {
+        ajax_options.data = data;
+    }
+        
+    var xhr = $.ajax(ajax_options);
+
+    xhr.getValidJSONConfig = config;
+    return xhr;
+};
+
+function gotValidJSON (xhr, status) {
+    var json = eval("("+xhr.responseText+")");
+    if (xhr.getValidJSONConfig != null) {
+        if (xhr.getValidJSONConfig.schema.receive) {
+            var valid = JSONSchema.validate(json, validJSONSchemas[xhr.getValidJSONConfig.schema.receive]);
+        } else {
+            var valid = JSONSchema.validate(json);
+        }
+        eval(xhr.getValidJSONConfig.callback+"(json,valid)");
+    } else {
+        throw("No data object was attached to the json response so no callback could be found");
+    }
+}

static/js/redbeard.js

+var cache = new CacheProvider;
+
+function removeFallbacks() {
+    var query = $('.jstest');
+    if (query.length) {
+        query.remove();
+    }
+}
+
+var Key = Backbone.Model.extend({});
+
+var KeyCollection = Backbone.Collection.extend({
+    model: Key,
+    comparator: function(item) {
+        return item.get('key');
+    }
+});
+
+var IndexView = Backbone.View.extend({
+    el: $('#keylist'),
+    indexTemplate: $("#keyTemplate").template(),
+
+    render: function() {
+        removeFallbacks();
+        var sg = this;
+
+        this.el.fadeOut('fast', function() {
+            sg.el.empty();
+            $.tmpl(sg.indexTemplate, sg.model.toArray()).appendTo(sg.el);
+            sg.el.fadeIn('fast');
+        });
+        return this;
+    }
+});
+
+var KeyView = Backbone.View.extend({
+    el: $('#right'),
+    indexTemplate: $("#editTemplate").template(),
+
+    initialize: function(options) {},
+
+    render: function() {
+        var sg = this;
+        removeFallbacks();
+        this.el.fadeOut('fast', function() {
+            sg.el.empty();
+            $.tmpl(sg.indexTemplate, sg.model.toArray()).appendTo(sg.el);
+            sg.el.fadeIn('fast');
+        });
+        return this;
+    }
+});
+
+var KeyDatabase = Backbone.Controller.extend({
+    _index: null,
+    _keys: null,
+
+    routes: {
+        "": "index",
+        "key/:name": "keyview"
+    },
+
+    initialize: function(options) {
+        var ws = this;
+
+        if (this._index === null) {
+            $.ajax({
+                url: '/search/',
+                dataType: 'json',
+                data: {},
+                success: function(data) {
+                    ws._data = data;
+                    ws._keys = new KeyCollection(data['keys']);
+                    ws._index = new IndexView({model: ws._keys});
+                    Backbone.history.loadUrl();
+                }
+            });
+            return this;
+        }
+        return this;
+    },
+
+    index: function() {
+        this._index.render();
+    },
+
+    keyview: function(name) {
+        var ws = this;
+
+        $.ajax({
+            url: '/key/' + name + '/json',
+            dataType: 'json',
+            data: {},
+            success: function(data) {
+                ws._data = data;
+                ws._key = new KeyCollection(data['key']);
+                console.log(ws);
+                ws._index = new KeyView({model: ws._key}).render();
+                Backbone.history.loadUrl();
+            }
+        });
+        return this;
+    }
+});
+
+keydatabase = new KeyDatabase();
+Backbone.history.start();
+
+/*
 function listFilter(header, list) {
     var form = $('<form>').attr({'class': 'filterform', 'action': '#'}),
         input = $('<input>').attr({
         });
     });
 });
+*/

static/js/underscore-min.js

+// Underscore.js 1.1.4
+// (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
+// Underscore is freely distributable under the MIT license.
+// Portions of Underscore are inspired or borrowed from Prototype,
+// Oliver Steele's Functional, and John Resig's Micro-Templating.
+// For all details and documentation:
+// http://documentcloud.github.com/underscore
+(function(){var q=this,C=q._,m={},j=Array.prototype,n=Object.prototype,i=j.slice,D=j.unshift,E=n.toString,o=n.hasOwnProperty,s=j.forEach,t=j.map,u=j.reduce,v=j.reduceRight,w=j.filter,x=j.every,y=j.some,p=j.indexOf,z=j.lastIndexOf;n=Array.isArray;var F=Object.keys,c=function(a){return new l(a)};if(typeof module!=="undefined"&&module.exports){module.exports=c;c._=c}else q._=c;c.VERSION="1.1.4";var k=c.each=c.forEach=function(a,b,d){if(a!=null)if(s&&a.forEach===s)a.forEach(b,d);else if(c.isNumber(a.length))for(var e=
+0,f=a.length;e<f;e++){if(b.call(d,a[e],e,a)===m)break}else for(e in a)if(o.call(a,e))if(b.call(d,a[e],e,a)===m)break};c.map=function(a,b,d){var e=[];if(a==null)return e;if(t&&a.map===t)return a.map(b,d);k(a,function(f,g,h){e[e.length]=b.call(d,f,g,h)});return e};c.reduce=c.foldl=c.inject=function(a,b,d,e){var f=d!==void 0;if(a==null)a=[];if(u&&a.reduce===u){if(e)b=c.bind(b,e);return f?a.reduce(b,d):a.reduce(b)}k(a,function(g,h,G){if(!f&&h===0){d=g;f=true}else d=b.call(e,d,g,h,G)});if(!f)throw new TypeError("Reduce of empty array with no initial value");
+return d};c.reduceRight=c.foldr=function(a,b,d,e){if(a==null)a=[];if(v&&a.reduceRight===v){if(e)b=c.bind(b,e);return d!==void 0?a.reduceRight(b,d):a.reduceRight(b)}a=(c.isArray(a)?a.slice():c.toArray(a)).reverse();return c.reduce(a,b,d,e)};c.find=c.detect=function(a,b,d){var e;A(a,function(f,g,h){if(b.call(d,f,g,h)){e=f;return true}});return e};c.filter=c.select=function(a,b,d){var e=[];if(a==null)return e;if(w&&a.filter===w)return a.filter(b,d);k(a,function(f,g,h){if(b.call(d,f,g,h))e[e.length]=
+f});return e};c.reject=function(a,b,d){var e=[];if(a==null)return e;k(a,function(f,g,h){b.call(d,f,g,h)||(e[e.length]=f)});return e};c.every=c.all=function(a,b,d){b=b||c.identity;var e=true;if(a==null)return e;if(x&&a.every===x)return a.every(b,d);k(a,function(f,g,h){if(!(e=e&&b.call(d,f,g,h)))return m});return e};var A=c.some=c.any=function(a,b,d){b=b||c.identity;var e=false;if(a==null)return e;if(y&&a.some===y)return a.some(b,d);k(a,function(f,g,h){if(e=b.call(d,f,g,h))return m});return e};c.include=
+c.contains=function(a,b){var d=false;if(a==null)return d;if(p&&a.indexOf===p)return a.indexOf(b)!=-1;A(a,function(e){if(d=e===b)return true});return d};c.invoke=function(a,b){var d=i.call(arguments,2);return c.map(a,function(e){return(b?e[b]:e).apply(e,d)})};c.pluck=function(a,b){return c.map(a,function(d){return d[b]})};c.max=function(a,b,d){if(!b&&c.isArray(a))return Math.max.apply(Math,a);var e={computed:-Infinity};k(a,function(f,g,h){g=b?b.call(d,f,g,h):f;g>=e.computed&&(e={value:f,computed:g})});
+return e.value};c.min=function(a,b,d){if(!b&&c.isArray(a))return Math.min.apply(Math,a);var e={computed:Infinity};k(a,function(f,g,h){g=b?b.call(d,f,g,h):f;g<e.computed&&(e={value:f,computed:g})});return e.value};c.sortBy=function(a,b,d){return c.pluck(c.map(a,function(e,f,g){return{value:e,criteria:b.call(d,e,f,g)}}).sort(function(e,f){var g=e.criteria,h=f.criteria;return g<h?-1:g>h?1:0}),"value")};c.sortedIndex=function(a,b,d){d=d||c.identity;for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(b)?
+e=g+1:f=g}return e};c.toArray=function(a){if(!a)return[];if(a.toArray)return a.toArray();if(c.isArray(a))return a;if(c.isArguments(a))return i.call(a);return c.values(a)};c.size=function(a){return c.toArray(a).length};c.first=c.head=function(a,b,d){return b&&!d?i.call(a,0,b):a[0]};c.rest=c.tail=function(a,b,d){return i.call(a,c.isUndefined(b)||d?1:b)};c.last=function(a){return a[a.length-1]};c.compact=function(a){return c.filter(a,function(b){return!!b})};c.flatten=function(a){return c.reduce(a,function(b,
+d){if(c.isArray(d))return b.concat(c.flatten(d));b[b.length]=d;return b},[])};c.without=function(a){var b=i.call(arguments,1);return c.filter(a,function(d){return!c.include(b,d)})};c.uniq=c.unique=function(a,b){return c.reduce(a,function(d,e,f){if(0==f||(b===true?c.last(d)!=e:!c.include(d,e)))d[d.length]=e;return d},[])};c.intersect=function(a){var b=i.call(arguments,1);return c.filter(c.uniq(a),function(d){return c.every(b,function(e){return c.indexOf(e,d)>=0})})};c.zip=function(){for(var a=i.call(arguments),
+b=c.max(c.pluck(a,"length")),d=Array(b),e=0;e<b;e++)d[e]=c.pluck(a,""+e);return d};c.indexOf=function(a,b,d){if(a==null)return-1;if(d){d=c.sortedIndex(a,b);return a[d]===b?d:-1}if(p&&a.indexOf===p)return a.indexOf(b);d=0;for(var e=a.length;d<e;d++)if(a[d]===b)return d;return-1};c.lastIndexOf=function(a,b){if(a==null)return-1;if(z&&a.lastIndexOf===z)return a.lastIndexOf(b);for(var d=a.length;d--;)if(a[d]===b)return d;return-1};c.range=function(a,b,d){var e=i.call(arguments),f=e.length<=1;a=f?0:e[0];
+b=f?e[0]:e[1];d=e[2]||1;e=Math.max(Math.ceil((b-a)/d),0);f=0;for(var g=Array(e);f<e;){g[f++]=a;a+=d}return g};c.bind=function(a,b){var d=i.call(arguments,2);return function(){return a.apply(b||{},d.concat(i.call(arguments)))}};c.bindAll=function(a){var b=i.call(arguments,1);if(b.length==0)b=c.functions(a);k(b,function(d){a[d]=c.bind(a[d],a)});return a};c.memoize=function(a,b){var d={};b=b||c.identity;return function(){var e=b.apply(this,arguments);return e in d?d[e]:d[e]=a.apply(this,arguments)}};
+c.delay=function(a,b){var d=i.call(arguments,2);return setTimeout(function(){return a.apply(a,d)},b)};c.defer=function(a){return c.delay.apply(c,[a,1].concat(i.call(arguments,1)))};var B=function(a,b,d){var e;return function(){var f=this,g=arguments,h=function(){e=null;a.apply(f,g)};d&&clearTimeout(e);if(d||!e)e=setTimeout(h,b)}};c.throttle=function(a,b){return B(a,b,false)};c.debounce=function(a,b){return B(a,b,true)};c.wrap=function(a,b){return function(){var d=[a].concat(i.call(arguments));return b.apply(this,
+d)}};c.compose=function(){var a=i.call(arguments);return function(){for(var b=i.call(arguments),d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};c.keys=F||function(a){if(c.isArray(a))return c.range(0,a.length);var b=[],d;for(d in a)if(o.call(a,d))b[b.length]=d;return b};c.values=function(a){return c.map(a,c.identity)};c.functions=c.methods=function(a){return c.filter(c.keys(a),function(b){return c.isFunction(a[b])}).sort()};c.extend=function(a){k(i.call(arguments,1),function(b){for(var d in b)a[d]=
+b[d]});return a};c.clone=function(a){return c.isArray(a)?a.slice():c.extend({},a)};c.tap=function(a,b){b(a);return a};c.isEqual=function(a,b){if(a===b)return true;var d=typeof a;if(d!=typeof b)return false;if(a==b)return true;if(!a&&b||a&&!b)return false;if(a._chain)a=a._wrapped;if(b._chain)b=b._wrapped;if(a.isEqual)return a.isEqual(b);if(c.isDate(a)&&c.isDate(b))return a.getTime()===b.getTime();if(c.isNaN(a)&&c.isNaN(b))return false;if(c.isRegExp(a)&&c.isRegExp(b))return a.source===b.source&&a.global===
+b.global&&a.ignoreCase===b.ignoreCase&&a.multiline===b.multiline;if(d!=="object")return false;if(a.length&&a.length!==b.length)return false;d=c.keys(a);var e=c.keys(b);if(d.length!=e.length)return false;for(var f in a)if(!(f in b)||!c.isEqual(a[f],b[f]))return false;return true};c.isEmpty=function(a){if(c.isArray(a)||c.isString(a))return a.length===0;for(var b in a)if(o.call(a,b))return false;return true};c.isElement=function(a){return!!(a&&a.nodeType==1)};c.isArray=n||function(a){return E.call(a)===
+"[object Array]"};c.isArguments=function(a){return!!(a&&o.call(a,"callee"))};c.isFunction=function(a){return!!(a&&a.constructor&&a.call&&a.apply)};c.isString=function(a){return!!(a===""||a&&a.charCodeAt&&a.substr)};c.isNumber=function(a){return!!(a===0||a&&a.toExponential&&a.toFixed)};c.isNaN=function(a){return a!==a};c.isBoolean=function(a){return a===true||a===false};c.isDate=function(a){return!!(a&&a.getTimezoneOffset&&a.setUTCFullYear)};c.isRegExp=function(a){return!!(a&&a.test&&a.exec&&(a.ignoreCase||
+a.ignoreCase===false))};c.isNull=function(a){return a===null};c.isUndefined=function(a){return a===void 0};c.noConflict=function(){q._=C;return this};c.identity=function(a){return a};c.times=function(a,b,d){for(var e=0;e<a;e++)b.call(d,e)};c.mixin=function(a){k(c.functions(a),function(b){H(b,c[b]=a[b])})};var I=0;c.uniqueId=function(a){var b=I++;return a?a+b:b};c.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g};c.template=function(a,b){var d=c.templateSettings;d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+
+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.interpolate,function(e,f){return"',"+f.replace(/\\'/g,"'")+",'"}).replace(d.evaluate||null,function(e,f){return"');"+f.replace(/\\'/g,"'").replace(/[\r\n\t]/g," ")+"__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');";d=new Function("obj",d);return b?d(b):d};var l=function(a){this._wrapped=a};c.prototype=l.prototype;var r=function(a,b){return b?c(a).chain():a},H=function(a,b){l.prototype[a]=function(){var d=
+i.call(arguments);D.call(d,this._wrapped);return r(b.apply(c,d),this._chain)}};c.mixin(c);k(["pop","push","reverse","shift","sort","splice","unshift"],function(a){var b=j[a];l.prototype[a]=function(){b.apply(this._wrapped,arguments);return r(this._wrapped,this._chain)}});k(["concat","join","slice"],function(a){var b=j[a];l.prototype[a]=function(){return r(b.apply(this._wrapped,arguments),this._chain)}});l.prototype.chain=function(){this._chain=true;return this};l.prototype.value=function(){return this._wrapped}})();

templates/index.html

 
 {% block content %}
 {% endblock %}
-
-{% block extra_scripts %}
-	<script src="{{ url_for('static', filename='js/jquery.hashchange.min.js') }}"></script>
-	<script src="{{ url_for('static', filename='js/jquery.scrollTo-1.4.2-min.js') }}"></script>
-	<script src="{{ url_for('static', filename='js/redbeard.js') }}"></script>
-{% endblock %}

templates/layout.html

 	<link rel="stylesheet" href="{{ url_for('static', filename='css/html5reset-1.6.1.css') }}" media="screen" charset="utf-8">
 	<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" media="screen" charset="utf-8">
 	<link rel="stylesheet" href="{{ url_for('static', filename='css/jquery.confirm.css') }}" media="screen" charset="utf-8">
-	<script type="text/javascript" src="{{ url_for('static', filename='js/jquery-1.5.1.min.js') }}"></script>
-	<script type="text/javascript" src="{{ url_for('static', filename='js/modernizr-1.6.min.js') }}"></script>
 </head>
 <body>
 	<header id="masthead">
 			
 
 	<div id="left">
+		<div class="jstest">This application is running with Javascript turned 
+			off.</div>
+		<header id="keyheader">
+			<h1>Keys</h1>
+		</header>
+		<ul id="keylist"></ul>
 		{% block leftblock %}{% endblock %}
 	</div>
 
 	<p>&copy; <a href="http://brack3t.com">Brack3t</a>. Design by <a href="http://www.changemodedesign.com/">Changemode Design</a>. Icons from <a 
 			href="http://somerandomdude.com/projects/iconic/">Iconic</a>.</p>
 	</footer>
-	<script src="{{ url_for('static', filename='js/jquery.confirm.js') }}"></script>
+	<script src="{{ url_for('static', filename='js/LAB.min.js') }}"></script>
+	<script>
+		$LAB
+			.script("{{ url_for('static', filename='js/jquery-1.5.1.min.js') }}").wait()
+			.script("{{ url_for('static', filename='js/modernizr-1.6.min.js') }}")
+			.script("{{ url_for('static', filename='js/jquery.tmpl.min.js') }}")
+			.script("{{ url_for('static', filename='js/underscore-min.js') }}")
+			.script("{{ url_for('static', filename='js/backbone-min.js') }}")
+			.script("{{ url_for('static', filename='js/cacheprovider.js') }}")
+			.script("{{ url_for('static', filename='js/jquery.confirm.js') }}")
+			.script("{{ url_for('static', filename='js/jquery.hashchange.min.js') }}")
+			.script("{{ url_for('static', filename='js/jquery.scrollTo-1.4.2-min.js') }}").wait()
+			.script("{{ url_for('static', filename='js/redbeard.js') }}");
+	</script>
+	<script type="text/x-jquery-tmpl" id="indexTemplate">
+		<header id="keyheader">
+			<h1>Keys</h1>
+		</header>
+		<ul id="keylist"></ul>
+	</script>
+	<script type="text/x-jquery-tmpl" id="keyTemplate">
+		<li><a href="#key/${attributes.key}">${attributes.key}</a></li>
+	</script>
+	<script type="text/x-jquery-tmpl" id="editTemplate">
+		<article>
+			<form action="{{ url_for('save', key=key) }}" method="post" id="key">
+				<header>
+					<h2>{{ rtype }}</h2>
+					<input type="text" name="key_name" value="${attribute.key}">
+					<input type="hidden" name="saved_key_name" value="${attribute.key}">
+					{% if ttl %}
+					<h3>Key expires in: {{ ttl }} seconds</h3>
+					{% endif %}
+				</header>
+				<textarea name="value" cols="60" rows="20">${attribute.value}</textarea>
+				<div id="controls">
+					<a id="refresh" href="{{ url_for('key', key=key) }}" title="refresh">refresh</a>
+					<a id="delete" href="{{ url_for('delete', key=key) }}" title="delete">delete</a>
+					<input type="submit" value="save" title="save">
+				</div>
+			</form>
+		</article>
+	</script>
 	{% block extra_scripts %}{% endblock %}
 </body>
 </html>