Michael Granger avatar Michael Granger committed e4c09fc

First experimental view via Spine.js

Comments (0)

Files changed (62)

 BEGIN {
     require 'pathname'
 	$LOAD_PATH.unshift( Pathname.new( __FILE__ ).dirname + 'lib' )
-	$LOAD_PATH.unshift( Pathname.new( __FILE__ ).dirname.parent + 'Strelka/lib' )
-	$LOAD_PATH.unshift( Pathname.new( __FILE__ ).dirname.parent + 'Mongrel2/lib' )
 }
 
 begin

data/strelka-admin/apps/admin-socket

+#!/usr/bin/env ruby
+# encoding: utf-8
+
+require 'pathname'
+require 'logger'
+require 'mongrel2/config'
+require 'mongrel2/logging'
+require 'mongrel2/handler'
+
+
+# A websocket that sends events about the Mongrel2 server as it's running.
+class AdminWebSocket < Mongrel2::Handler
+	include Mongrel2::WebSocket::Constants
+
+
+
+end # class AdminWebSocket
+
+AdminWebSocket.run
+

data/strelka-admin/apps/config-service

 #!/usr/bin/env ruby
 
-require 'configurability'
 require 'strelka'
+require 'strelka/admin'
 require 'uuidtools'
 
 # The Strelka mongrel2 config service app.
 class Strelka::ConfigService < Strelka::App
 	extend Configurability
 
-	# Piggyback on the 
-
 	# The version of this service API -- used in determining the
 	# versioned route. If this changes in a non-backward compatible way,
 	# this should be incremented
 	APIVERSION = 1
 
 	# The 'appid' of the route to point the application at
-	ID = Strelka::CONFIGSERVICE_ID
+	ID = Strelka::Admin::CONFIGSERVICE_ID
 
 	# Load some plugins
-	plugins :restresources
+	plugins :restresources, :templating
 
 	# By default, responses are HTML
 	default_type 'text/html'
 	# templates \
 	#	:overview => 'configservice/overview.tmpl'
 
+	param :id, :integer
+	param :uuid, :string
 
 	# GET /uuid -- fetch a plain-text random UUID
 	get '/uuid' do |req|
 	# Config Service API
 	#
 
-	def self::configure( * )
 	resource Mongrel2::Config::Server
 	resource Mongrel2::Config::Host
 	resource Mongrel2::Config::Route
 	resource Mongrel2::Config::Filter
 	resource Mongrel2::Config::Mimetype
 
-end # class Strelka::AdminConsole
+end # class Strelka::ConfigService
 
 
 if __FILE__ == $0
-	Strelka.load_config
+	Strelka.load_config( 'etc/config.yml' )
 	Strelka::ConfigService.run
 end
 

data/strelka-admin/apps/strelka-admin

-#!/usr/bin/env ruby
-
-require 'strelka'
-require 'strelka/admin'
-
-
-# The Strelka admin web console.
-class Strelka::AdminConsole < Strelka::App
-
-	# The 'appid' of the route to point the application at
-	ID = Strelka::ADMINSERVER_ID
-
-	# Load some plugins
-	plugins :templating, :routing, :filters, :errors
-
-
-	# By default, responses are HTML
-	default_type 'text/html'
-
-	#
-	# Templating
-	#
-
-	# Templating -- wrap everything in the layout
-	layout 'layout.tmpl'
-	templates \
-		:console => 'admin/console.tmpl',
-		:server  => 'admin/server.tmpl',
-		:host    => 'admin/host.tmpl',
-		:message => 'admin/message.tmpl'
-
-
-	#
-	# Filters
-	#
-
-	# Add some custom headers to all responses
-	filter( :response ) do |res|
-		res.headers.x_strelka_version = Strelka.version_string( true )
-	end
-
-
-	#
-	# Routes
-	#
-
-	### Initialize some application data.
-	def initialize( * )
-
-		# Commonalize all the loggers
-		Mongrel2.logger = Strelka.logger
-		Configurability.logger = Strelka.logger
-		Inversion.logger.level = Logger::WARN
-
-		@adminserver = Mongrel2::Config::Server.by_uuid( ADMINSERVER_ID ).first
-		@control = @adminserver.control_socket
-
-		super
-	end
-
-
-	#
-	# Custom Error Responses
-	#
-
-	# on_status 404, :missing
-	#
-	# on_status 302,304, :redirect
-
-
-
-	#
-	# App Routes
-	#
-
-	# GET / -- console view
-	get do |req|
-		tmpl = self.template( :console )
-		tmpl.request = req
-		tmpl.control = @control
-		tmpl.servers = Mongrel2::Config.servers
-
-		return tmpl
-	end
-
-end # class Strelka::AdminConsole
-
-
-if __FILE__ == $0
-	require 'configurability/config'
-
-	# Commonalize all the loggers
-	Mongrel2.logger = Strelka.logger
-	Configurability.logger = Strelka.logger
-	PluginFactory.logger = Strelka.logger
-
-	Inversion.logger.level = Logger::WARN
-
-	Strelka.load_config
-	Strelka::AdminConsole.run
-end
-

data/strelka-admin/apps/strelka-setup

-#!/usr/bin/env ruby
-
-require 'inversion'
-require 'strelka'
-
-# The Strelka setup web application.
-class Strelka::SetupProcess < Strelka::App
-
-	# NOTE:
-	# This is currently just a sketch for an idea of how Strelka could support stateful
-	# 'process' applications ala Tir's coroutine-based "natural" handlers. It's not
-	# implemented yet, so this will error:
-	plugins :process
-
-	layout 'setup/layout.tmpl'
-
-	views :step1 => 'setup/step1',
-	      :step2 => 'setup/step2'
-
-
-	### Progress through the setup process.
-	def main( req )
-		params = show( :step1 )
-		params = show( :step2, params )
-
-	end
-
-end # class Strelka::AdminConsole
-
-
-Strelka::SetupProcess.run( 'strelka-setup' )
-

data/strelka-admin/static/css/master.css

-/* @override http://localhost:7337/css/master.css */
+/* @override http://localhost:7337/css/master.css
+	http://localhost:8113/css/master.css */
 
-@import url(reset.css);
+@import url(/css/bootstrap.css);
+body {
+	padding-top: 60px;
+	padding-bottom: 40px;
+}
+.sidebar-nav {
+	padding: 9px 0;
+}
+@import url(/css/bootstrap-responsive.css);
+
 
 /* @group Fonts */
 @font-face {
 
 /* @group Generic Element Styles */
 html {
-	margin: 0;
 	font: 14px/18px IstokWeb;
-	background-color: #999;
-	color: #333;
 }
 
-body {
-	margin: 0;
-	padding: 0;
-	min-width: 800px;
-}
-
-
-h1,h2,h3,h4,h5,h6 {
-	font-weight: bold;
-	color: black;
-}
-
-a {
-	color: #911;
-	text-decoration: none;
-	background: rgba( 255,255,255, 0.15 );
-}
-/* @end */
-
-
-/* @group Top-level Sections and Headers */
-body > header,
-body > section {
-    background: #cbcfd5 url(../images/noise.png) repeat;
-}
-body > header {
-	padding: 40px 40px 20px;
-	font-weight: bold;
-	font-size: 60px;
-	text-shadow: 0 0 8px rgba( 25,25,25, 0.2 );
-}
-body > header h1 {
-	margin: 0;
-	color: white;
-}
-body > section {
-	padding: 0 40px 1em;
-}
-
-/* @end */
-
-/* @group Top-level Nav */
-body > nav {
-	margin-top: -0.5em;
-    background: #b5b9be;
-    padding: 0.5em 50px;
-}
-body > nav ol li {
-	display: inline;
-}
-body > nav ol li + li:before {
-	content: "> ";
-	color: whitesmoke;
-}
-/* @end */
-
-
-
-/* @group Footers */
-body > footer {
-	color: #777;
-	font-size: 0.9em;
-	padding: 1em;
-	background: -moz-linear-gradient(top, 
-		rgba(0,0,0,0.85) 0%, 
-		rgba(0,0,0,0.4) 7%, 
-		rgba(0,0,0,0) 39%, 
-		rgba(0,0,0,0) 100%); /* FF3.6+ */
-	background: -webkit-gradient(linear, left top, left bottom, 
-		color-stop(0%,rgba(0,0,0,0.85)), 
-		color-stop(7%,rgba(0,0,0,0.4)), 
-		color-stop(39%,rgba(0,0,0,0)), 
-		color-stop(100%,rgba(0,0,0,0))); /* Chrome,Safari4+ */
-	background: -webkit-linear-gradient(top, 
-		rgba(0,0,0,0.85) 0%,
-		rgba(0,0,0,0.4) 7%,
-		rgba(0,0,0,0) 39%,
-		rgba(0,0,0,0) 100%); /* Chrome10+,Safari5.1+ */
-	background: -o-linear-gradient(top, 
-		rgba(0,0,0,0.85) 0%,
-		rgba(0,0,0,0.4) 7%,
-		rgba(0,0,0,0) 39%,
-		rgba(0,0,0,0) 100%); /* Opera11.10+ */
-	background: -ms-linear-gradient(top, 
-		rgba(0,0,0,0.85) 0%,
-		rgba(0,0,0,0.4) 7%,
-		rgba(0,0,0,0) 39%,
-		rgba(0,0,0,0) 100%); /* IE10+ */
-	filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#d9000000', 
-		endColorstr='#00000000',GradientType=0 ); /* IE6-9 */
-	background: linear-gradient(top, 
-		rgba(0,0,0,0.85) 0%,
-		rgba(0,0,0,0.4) 7%,
-		rgba(0,0,0,0) 39%,
-		rgba(0,0,0,0) 100%); /* W3C */
-}
-footer span.copyright {
-	float: right;
-}
-/* @end */
-
-
-/* @group Second-Level sections and headers */
-body > section > section {
-	padding: 2em 0;
-}
-section > header {
-	padding: 0.5em 0;
-	font-size: 1.2em;
-}
-/* Add space between sibing sections and tables */
-section + section,
-table + section,
-section + table {
-	margin-top: 2em;
-}
-/* @end */
-
-
-/* @group Tables */
-table {}
-table caption {
-	caption-side: bottom;
-	padding: 1em;
-	font-size: 0.9em;
-	color: #777;
-}
-table caption p {
-	text-align: center;
-}
-td, th {
-	padding: 2px 1em;
-}
-td {
-	border-bottom: 1px dotted #999;
-}
-th {
-	font-weight: bold;
-	border-bottom: 2px solid;
-}
-table.horizontal th {
-	border-bottom: 1px dotted #999;
-	text-align: right;
-}
-td.icon {
-	text-align: center;
-	line-height: 8px;
-	vertical-align: bottom;
-}
-
-tbody.actions td {
-	border: none;
-	padding: 0;
-	margin: 0;
-}
-tbody.actions nav {
-	text-align: right;
-}
-tbody.actions nav button {
-	font-size: 1.1em;
-	padding: 0.5em 1em;
-	margin: 1em 0;
-	border: 1px;
-	background: #45484d;
-	background: -moz-linear-gradient(top, #45484d 0%, #000000 100%);
-	background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#45484d), color-stop(100%,#000000));
-	background: -webkit-linear-gradient(top, #45484d 0%,#000000 100%);
-	background: -o-linear-gradient(top, #45484d 0%,#000000 100%);
-	background: -ms-linear-gradient(top, #45484d 0%,#000000 100%);
-	background: linear-gradient(top, #45484d 0%,#000000 100%);
-    display: inline-block; 
-    padding: 5px 10px 6px; 
-    color: #fff; 
-    text-decoration: none;
-    -moz-border-radius: 5px; 
-    -webkit-border-radius: 5px;
-    -moz-box-shadow: 0 1px 3px rgba(0,0,0,0.5);
-    -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.5);
-    text-shadow: 0 -1px 1px rgba(0,0,0,0.25);
-    border-bottom: 1px solid rgba(0,0,0,0.25);
-    position: relative;
-    cursor: pointer;
-}
-
-
-/* @group Editable row affordance */
-tr.unsaved {}
-tr.unsaved td {}
-
-tr.unsaved td.editable,
-tr.unsaved td.toggle {
-	font-style: italic;
-	background: white;
-	cursor: pointer;
-}
-tr.unsaved td.controls {
-	visibility: visible;
-}
-
-.editable form {
-	margin: 0;
-	padding: 0;
-	display: inline-block;
-}
-.editable form input {
-	margin: 0;
-	padding: 0 4px;
-	border: 1px solid #efefef;
-	background: #efefef;
-	border-radius: 0.5em;
-	outline: none;
-	max-height: 1em;
-}
-.editable form input:focus {
-	outline: none;
-}
-
-td.toggle img {
-	visibility: hidden;
-}
-td.toggle.enabled img {
-	visibility: visible;
-}
-
-tr.server.unsaved span.target {
-	cursor: pointer;
-}
-
-td.toggle span.target {
-	border: 1px dashed rgba( 0,0,0, 0.20 );
-	border-radius: 4px;
-	display: inline-block;
-}
-/* @end */
-
-/* @group Controls Column */
-th.controls,
-td.controls {
-	border: none;
-}
-td.controls {
-	visibility: hidden;
-}
-td.controls button {
-	padding: 0;
-	margin: 0;
-	border: none;
-	background: clear;
-	cursor: pointer;
-}
-/* @end */
-/* @end */
-
-
-/* @group Specific Application Section Styles */
-section#body {
-	min-height: 600px;
-}
-section#versions ul {
-	font-family: Inconsolata, fixed;
-}
-section#tasklist tr.system-task {
-	font-style: italic;
-}
-
-section#servers table {
-	width: 90%;
-}
-
-section#routes td.target {
-	background: rgba( 255,255,255, 0.15 );
-}
-section#routes span.send-ident {
-	font-weight: bold;
-}
-section#routes span.send-spec,
-section#routes span.recv-spec {
-	background-color: rgba( 255,255,255, 0.5 );
-	padding: 0 4px;
-	border-radius: 0.25em;
-}
-section#routes span.index-file {
+div.container-fluid > footer {
 	color: #999;
-}
-section#routes span.default-ctype {
-	font-family: Inconsolata, monospace;
-	font-size: 0.8em;
-}
-/* @end */
-
+}
Add a comment to this file

data/strelka-admin/static/images/21d1-16.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/21d1.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/glyphicons-halflings-white.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/glyphicons-halflings.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/icons/blank.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/icons/cross-button.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/icons/tick-button.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/icons/tick-circle-frame.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/icons/tick-circle.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/icons/tick-octagon-frame.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/icons/tick-octagon.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/icons/tick-red.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/icons/tick-shield.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/icons/tick-small-circle.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/icons/tick-small-red.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/icons/tick-small-white.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/icons/tick-small.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/icons/tick-white.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/icons/tick.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/noise.jpg

Removed
Old image
Add a comment to this file

data/strelka-admin/static/images/noise.png

Removed
Old image
Add a comment to this file

data/strelka-admin/static/img/21d1-16.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/21d1.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/glyphicons-halflings-white.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/glyphicons-halflings.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/icons/blank.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/icons/cross-button.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/icons/tick-button.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/icons/tick-circle-frame.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/icons/tick-circle.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/icons/tick-octagon-frame.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/icons/tick-octagon.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/icons/tick-red.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/icons/tick-shield.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/icons/tick-small-circle.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/icons/tick-small-red.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/icons/tick-small-white.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/icons/tick-small.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/icons/tick-white.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/icons/tick.png

Added
New image
Add a comment to this file

data/strelka-admin/static/img/noise.jpg

Added
New image
Add a comment to this file

data/strelka-admin/static/img/noise.png

Added
New image

data/strelka-admin/static/index.html

+<!DOCTYPE html>
+<html lang="en">
+<head>
+	<meta charset="utf-8">
+	<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+	<meta name="description" content="The Strelka Admin Console">
+	<meta name="author" content="Michael Granger">
+
+	<title>Strelka Web Admin Console</title>
+
+	<link rel="shortcut icon" type="image/png" href="/images/21d1-16.png" />
+	<link rel="stylesheet" href="/css/master.css" type="text/css" media="screen"
+		title="no title" charset="utf-8" />
+
+	<!--[if lt IE 9]>
+		<script src="//html5shim.googlecode.com/svn/trunk/html5.js"></script>
+		<![endif]-->
+
+	<script src="/js/vendor/jquery-1.7.1.js" type="text/javascript" charset="utf-8" defer="defer"></script>
+	<script src="/js/vendor/jquery.tmpl.min.js" type="text/javascript" charset="utf-8" defer="defer"></script>
+	<script src="/js/vendor/spine/spine.js" type="text/javascript" charset="utf-8" defer="defer"></script>
+	<script src="/js/vendor/spine/local.js" type="text/javascript" charset="utf-8" defer="defer"></script>
+	<script src="/js/vendor/spine/ajax.js" type="text/javascript" charset="utf-8" defer="defer"></script>
+	<script src="/js/vendor/spine/manager.js" type="text/javascript" charset="utf-8" defer="defer"></script>
+	<script src="/js/vendor/spine/tmpl.js" type="text/javascript" charset="utf-8" defer="defer"></script>
+	<script src="/js/vendor/bootstrap.min.js" type="text/javascript" charset="utf-8" defer="defer"></script>
+	<script src="/js/admin/app.js" type="text/javascript" charset="utf-8" defer="defer"></script>
+
+</head>
+<body>
+	<nav id="global-navbar" class="navbar navbar-fixed-top">
+		<div class="navbar-inner">
+			<div class="container-fluid">
+				<h1><a class="brand" href="#">Стрелке Admin Console</a></h1>
+				<div class="nav-collapse">
+					<ul class="nav">
+						<li class="active"><a href="#">Overview</a></li>
+						<li><a href="#about">Config</a></li>
+						<li><a href="#contact">Cluster</a></li>
+					</ul>
+					<p class="navbar-text pull-right">Logged in as <a href="#">username</a></p>
+				</div>
+			</div>
+		</div>
+	</nav>
+
+	<div class="container-fluid">
+		<div class="row-fluid">
+			<header id="herobar" class="span12">
+				<h2>Servers</h2>
+			</header>
+		</div>
+
+		<div class="row-fluid">
+			<section id="content" class="span12">
+
+			<table id="servers" class="table servers-table">
+			<thead>
+				<tr>
+					<th>ID</th>
+					<th>Name</th>
+					<th>Bind Addr</th>
+					<th>Port</th>
+					<th>SSL?</th>
+				</tr>
+			</thead>
+			<tbody>
+			<script id="server-template" type="text/x-jquery-tmpl">
+				<td class="server-uuid">${uuid}</td>
+				<td class="server-name">${name}</td>
+				<td class="server-bind-addr">${bind_addr}</td>
+				<td class="server-port">${port}</td>
+				{{if use_ssl}}
+				<td class="server-ssl"><i class="icon-ok"></i></td>
+				{{else}}
+				<td class="server-ssl"><i class="icon-remove"></i></td>
+				{{/if}}
+			</script>
+			</tbody>
+			</table>
+
+			</section>
+		</div>
+
+		<hr>
+
+		<footer>
+			<p>
+				<span class="vcs-id">$Id$</span>
+				<span class="copyright">Copyright © 2012, Michael Granger</span>
+			</p>
+		</footer>
+	</div>
+
+
+</body>
+</html>
+

data/strelka-admin/static/js/admin/app.js

 *
 */
 
-const ConfigService = '/api/v1';
+(function(Spine, $, exports){
 
-var App = {
-	Models: {},
-	Collections: {},
-	Views: {},
-	Controllers: {},
+	var Server = Spine.Model.sub();
+	Server.configure( 'Server',
+		'uuid',
+		'access_log',
+		'error_log',
+		'chroot',
+		'pid_file',
+		'default_host',
+		'name',
+		'bind_addr',
+		'port',
+		'use_ssl'
+	);
 
-	init: function() {
+	Server.extend( Spine.Model.Ajax );
+	Server.extend({ url: "/api/v1/servers" });
 
-		Backbone.history.start();
-	}
-};
+	var ServerItem = Spine.Controller.sub({
 
-App.VERSION = '0.0.1';
+		tag: 'tr',
 
-/**
- * Models
- */
-App.Models.Server = Backbone.Model.extend();
-App.Collections.Servers = Backbone.Collection.extend({
-	model: App.Models.Server,
-	url: function () {
-		return this.document.location.origin + ConfigService + '/servers';
-	}
-});
+		// Delegate the click event to a local handler
+		events: {
+			"click": "click"
+		},
 
+		// Bind events to the record
+		init: function() {
+			if ( !this.item ) throw "@item required";
+			this.item.bind( "update", this.proxy(this.render) );
+			this.item.bind( "destroy", this.proxy(this.remove) );
+		},
 
+		render: function(item){
+			if (item) this.item = item;
 
-/**
- * Views
- */
-App.Views.ServerListView = Backbone.View.extend({
+			this.html( this.template(this.item) );
+			return this;
+		},
 
-	tagName: 'table',
-	template: _.template( '#server-template' );
+		// Use a template, in this case via jQuery.tmpl.js
+		template: function(items){
+			return $('#server-template').tmpl( items );
+		},
 
-	initialize: function () {
-		this.model.bind( "reset", this.render, this );
-	},
+		// Called after an element is destroyed
+		remove: function() {
+			this.el.remove();
+		},
 
-	render: function (eventName) {
-		_.each(this.model.models, function (server) {
-			$( this.el ).find( 'tbody' ).
-				append( this.template(server.toJSON()) );
-		}, this);
-		return this;
-	}
+		click: function() {}
+	});
 
-});
+	var Servers = Spine.Controller.sub({
+		init: function(){
+			Server.bind("refresh", this.proxy(this.addAll));
+			Server.bind("create",  this.proxy(this.addOne));
+		},
 
+		addOne: function(item){
+			var server = new ServerItem({item: item});
+			this.append(server.render());
+		},
+
+		addAll: function(){
+			Server.each(this.proxy(this.addOne));
+		}
+	});
+
+	exports.Server = Server;
+	exports.Servers = Servers;
+	exports.ServerItem = ServerItem;
+
+	var serversView = new Servers({
+		el: $('#servers tbody'),
+		className: 'server'
+	});
+
+})(Spine, Spine.$, window);
+
+$(document).ready(function() { Server.fetch() });
+
+

data/strelka-admin/static/js/vendor/spine/ajax.js

+(function() {
+  var $, Ajax, Base, Collection, Extend, Include, Model, Singleton,
+    __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
+    __hasProp = Object.prototype.hasOwnProperty,
+    __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; },
+    __slice = Array.prototype.slice;
+
+  if (typeof Spine === "undefined" || Spine === null) Spine = require('spine');
+
+  $ = Spine.$;
+
+  Model = Spine.Model;
+
+  Ajax = {
+    getURL: function(object) {
+      return object && (typeof object.url === "function" ? object.url() : void 0) || object.url;
+    },
+    enabled: true,
+    pending: false,
+    requests: [],
+    disable: function(callback) {
+      if (this.enabled) {
+        this.enabled = false;
+        callback();
+        return this.enabled = true;
+      } else {
+        return callback();
+      }
+    },
+    requestNext: function() {
+      var next;
+      next = this.requests.shift();
+      if (next) {
+        return this.request(next);
+      } else {
+        return this.pending = false;
+      }
+    },
+    request: function(callback) {
+      var _this = this;
+      return (callback()).complete(function() {
+        return _this.requestNext();
+      });
+    },
+    queue: function(callback) {
+      if (!this.enabled) return;
+      if (this.pending) {
+        this.requests.push(callback);
+      } else {
+        this.pending = true;
+        this.request(callback);
+      }
+      return callback;
+    }
+  };
+
+  Base = (function() {
+
+    function Base() {}
+
+    Base.prototype.defaults = {
+      contentType: 'application/json',
+      dataType: 'json',
+      processData: false,
+      headers: {
+        'X-Requested-With': 'XMLHttpRequest'
+      }
+    };
+
+    Base.prototype.ajax = function(params, defaults) {
+      return $.ajax($.extend({}, this.defaults, defaults, params));
+    };
+
+    Base.prototype.queue = function(callback) {
+      return Ajax.queue(callback);
+    };
+
+    return Base;
+
+  })();
+
+  Collection = (function(_super) {
+
+    __extends(Collection, _super);
+
+    function Collection(model) {
+      this.model = model;
+      this.errorResponse = __bind(this.errorResponse, this);
+      this.recordsResponse = __bind(this.recordsResponse, this);
+    }
+
+    Collection.prototype.find = function(id, params) {
+      var record;
+      record = new this.model({
+        id: id
+      });
+      return this.ajax(params, {
+        type: 'GET',
+        url: Ajax.getURL(record)
+      }).success(this.recordsResponse).error(this.errorResponse);
+    };
+
+    Collection.prototype.all = function(params) {
+      return this.ajax(params, {
+        type: 'GET',
+        url: Ajax.getURL(this.model)
+      }).success(this.recordsResponse).error(this.errorResponse);
+    };
+
+    Collection.prototype.fetch = function(params, options) {
+      var id,
+        _this = this;
+      if (params == null) params = {};
+      if (options == null) options = {};
+      if (id = params.id) {
+        delete params.id;
+        return this.find(id, params).success(function(record) {
+          return _this.model.refresh(record, options);
+        });
+      } else {
+        return this.all(params).success(function(records) {
+          return _this.model.refresh(records, options);
+        });
+      }
+    };
+
+    Collection.prototype.recordsResponse = function(data, status, xhr) {
+      return this.model.trigger('ajaxSuccess', null, status, xhr);
+    };
+
+    Collection.prototype.errorResponse = function(xhr, statusText, error) {
+      return this.model.trigger('ajaxError', null, xhr, statusText, error);
+    };
+
+    return Collection;
+
+  })(Base);
+
+  Singleton = (function(_super) {
+
+    __extends(Singleton, _super);
+
+    function Singleton(record) {
+      this.record = record;
+      this.errorResponse = __bind(this.errorResponse, this);
+      this.recordResponse = __bind(this.recordResponse, this);
+      this.model = this.record.constructor;
+    }
+
+    Singleton.prototype.reload = function(params, options) {
+      var _this = this;
+      return this.queue(function() {
+        return _this.ajax(params, {
+          type: 'GET',
+          url: Ajax.getURL(_this.record)
+        }).success(_this.recordResponse(options)).error(_this.errorResponse(options));
+      });
+    };
+
+    Singleton.prototype.create = function(params, options) {
+      var _this = this;
+      return this.queue(function() {
+        return _this.ajax(params, {
+          type: 'POST',
+          data: JSON.stringify(_this.record),
+          url: Ajax.getURL(_this.model)
+        }).success(_this.recordResponse(options)).error(_this.errorResponse(options));
+      });
+    };
+
+    Singleton.prototype.update = function(params, options) {
+      var _this = this;
+      return this.queue(function() {
+        return _this.ajax(params, {
+          type: 'PUT',
+          data: JSON.stringify(_this.record),
+          url: Ajax.getURL(_this.record)
+        }).success(_this.recordResponse(options)).error(_this.errorResponse(options));
+      });
+    };
+
+    Singleton.prototype.destroy = function(params, options) {
+      var _this = this;
+      return this.queue(function() {
+        return _this.ajax(params, {
+          type: 'DELETE',
+          url: Ajax.getURL(_this.record)
+        }).success(_this.recordResponse(options)).error(_this.errorResponse(options));
+      });
+    };
+
+    Singleton.prototype.recordResponse = function(options) {
+      var _this = this;
+      if (options == null) options = {};
+      return function(data, status, xhr) {
+        var _ref;
+        if (Spine.isBlank(data)) {
+          data = false;
+        } else {
+          data = _this.model.fromJSON(data);
+        }
+        Ajax.disable(function() {
+          if (data) {
+            if (data.id && _this.record.id !== data.id) {
+              _this.record.changeID(data.id);
+            }
+            return _this.record.updateAttributes(data.attributes());
+          }
+        });
+        _this.record.trigger('ajaxSuccess', data, status, xhr);
+        return (_ref = options.success) != null ? _ref.apply(_this.record) : void 0;
+      };
+    };
+
+    Singleton.prototype.errorResponse = function(options) {
+      var _this = this;
+      if (options == null) options = {};
+      return function(xhr, statusText, error) {
+        var _ref;
+        _this.record.trigger('ajaxError', xhr, statusText, error);
+        return (_ref = options.error) != null ? _ref.apply(_this.record) : void 0;
+      };
+    };
+
+    return Singleton;
+
+  })(Base);
+
+  Model.host = '';
+
+  Include = {
+    ajax: function() {
+      return new Singleton(this);
+    },
+    url: function() {
+      var args, url;
+      args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+      url = Ajax.getURL(this.constructor);
+      if (url.charAt(url.length - 1) !== '/') url += '/';
+      url += encodeURIComponent(this.id);
+      args.unshift(url);
+      return args.join('/');
+    }
+  };
+
+  Extend = {
+    ajax: function() {
+      return new Collection(this);
+    },
+    url: function() {
+      var args;
+      args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+      args.unshift(this.className.toLowerCase() + 's');
+      args.unshift(Model.host);
+      return args.join('/');
+    }
+  };
+
+  Model.Ajax = {
+    extended: function() {
+      this.fetch(this.ajaxFetch);
+      this.change(this.ajaxChange);
+      this.extend(Extend);
+      return this.include(Include);
+    },
+    ajaxFetch: function() {
+      var _ref;
+      return (_ref = this.ajax()).fetch.apply(_ref, arguments);
+    },
+    ajaxChange: function(record, type, options) {
+      if (options == null) options = {};
+      if (options.ajax === false) return;
+      return record.ajax()[type](options.ajax, options);
+    }
+  };
+
+  Model.Ajax.Methods = {
+    extended: function() {
+      this.extend(Extend);
+      return this.include(Include);
+    }
+  };
+
+  Ajax.defaults = Base.prototype.defaults;
+
+  Spine.Ajax = Ajax;
+
+  if (typeof module !== "undefined" && module !== null) module.exports = Ajax;
+
+}).call(this);

data/strelka-admin/static/js/vendor/spine/list.js

+(function() {
+  var $,
+    __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
+    __hasProp = Object.prototype.hasOwnProperty,
+    __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; };
+
+  if (typeof Spine === "undefined" || Spine === null) Spine = require('spine');
+
+  $ = Spine.$;
+
+  Spine.List = (function(_super) {
+
+    __extends(List, _super);
+
+    List.prototype.events = {
+      'click .item': 'click'
+    };
+
+    List.prototype.selectFirst = false;
+
+    function List() {
+      this.change = __bind(this.change, this);      List.__super__.constructor.apply(this, arguments);
+      this.bind('change', this.change);
+    }
+
+    List.prototype.template = function() {
+      return arguments[0];
+    };
+
+    List.prototype.change = function(item) {
+      this.current = item;
+      if (!this.current) {
+        this.children().removeClass('active');
+        return;
+      }
+      this.children().removeClass('active');
+      return this.children().forItem(this.current).addClass('active');
+    };
+
+    List.prototype.render = function(items) {
+      if (items) this.items = items;
+      this.html(this.template(this.items));
+      this.change(this.current);
+      if (this.selectFirst) {
+        if (!this.children('.active').length) {
+          return this.children(':first').click();
+        }
+      }
+    };
+
+    List.prototype.children = function(sel) {
+      return this.el.children(sel);
+    };
+
+    List.prototype.click = function(e) {
+      var item;
+      item = $(e.currentTarget).item();
+      this.trigger('change', item);
+      return true;
+    };
+
+    return List;
+
+  })(Spine.Controller);
+
+  if (typeof module !== "undefined" && module !== null) {
+    module.exports = Spine.List;
+  }
+
+}).call(this);

data/strelka-admin/static/js/vendor/spine/local.js

+(function() {
+
+  if (typeof Spine === "undefined" || Spine === null) Spine = require('spine');
+
+  Spine.Model.Local = {
+    extended: function() {
+      this.change(this.saveLocal);
+      return this.fetch(this.loadLocal);
+    },
+    saveLocal: function() {
+      var result;
+      result = JSON.stringify(this);
+      return localStorage[this.className] = result;
+    },
+    loadLocal: function() {
+      var result;
+      result = localStorage[this.className];
+      return this.refresh(result || [], {
+        clear: true
+      });
+    }
+  };
+
+  if (typeof module !== "undefined" && module !== null) {
+    module.exports = Spine.Model.Local;
+  }
+
+}).call(this);

data/strelka-admin/static/js/vendor/spine/manager.js

+(function() {
+  var $,
+    __hasProp = Object.prototype.hasOwnProperty,
+    __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; },
+    __slice = Array.prototype.slice;
+
+  if (typeof Spine === "undefined" || Spine === null) Spine = require('spine');
+
+  $ = Spine.$;
+
+  Spine.Manager = (function(_super) {
+
+    __extends(Manager, _super);
+
+    Manager.include(Spine.Events);
+
+    function Manager() {
+      this.controllers = [];
+      this.bind('change', this.change);
+      this.add.apply(this, arguments);
+    }
+
+    Manager.prototype.add = function() {
+      var cont, controllers, _i, _len, _results;
+      controllers = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+      _results = [];
+      for (_i = 0, _len = controllers.length; _i < _len; _i++) {
+        cont = controllers[_i];
+        _results.push(this.addOne(cont));
+      }
+      return _results;
+    };
+
+    Manager.prototype.addOne = function(controller) {
+      var _this = this;
+      controller.bind('active', function() {
+        var args;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        return _this.trigger.apply(_this, ['change', controller].concat(__slice.call(args)));
+      });
+      controller.bind('release', function() {
+        return _this.controllers.splice(_this.controllers.indexOf(controller), 1);
+      });
+      return this.controllers.push(controller);
+    };
+
+    Manager.prototype.deactivate = function() {
+      return this.trigger.apply(this, ['change', false].concat(__slice.call(arguments)));
+    };
+
+    Manager.prototype.change = function() {
+      var args, cont, current, _i, _len, _ref, _results;
+      current = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
+      _ref = this.controllers;
+      _results = [];
+      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+        cont = _ref[_i];
+        if (cont === current) {
+          _results.push(cont.activate.apply(cont, args));
+        } else {
+          _results.push(cont.deactivate.apply(cont, args));
+        }
+      }
+      return _results;
+    };
+
+    return Manager;
+
+  })(Spine.Module);
+
+  Spine.Controller.include({
+    active: function() {
+      var args;
+      args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+      if (typeof args[0] === 'function') {
+        this.bind('active', args[0]);
+      } else {
+        args.unshift('active');
+        this.trigger.apply(this, args);
+      }
+      return this;
+    },
+    isActive: function() {
+      return this.el.hasClass('active');
+    },
+    activate: function() {
+      this.el.addClass('active');
+      return this;
+    },
+    deactivate: function() {
+      this.el.removeClass('active');
+      return this;
+    }
+  });
+
+  Spine.Stack = (function(_super) {
+
+    __extends(Stack, _super);
+
+    Stack.prototype.controllers = {};
+
+    Stack.prototype.routes = {};
+
+    Stack.prototype.className = 'spine stack';
+
+    function Stack() {
+      var key, value, _fn, _ref, _ref2,
+        _this = this;
+      Stack.__super__.constructor.apply(this, arguments);
+      this.manager = new Spine.Manager;
+      this.manager.bind('change', function() {
+        var args, controller;
+        controller = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
+        if (controller) return _this.active.apply(_this, args);
+      });
+      _ref = this.controllers;
+      for (key in _ref) {
+        value = _ref[key];
+        this[key] = new value({
+          stack: this
+        });
+        this.add(this[key]);
+      }
+      _ref2 = this.routes;
+      _fn = function(key, value) {
+        var callback;
+        if (typeof value === 'function') callback = value;
+        callback || (callback = function() {
+          var _ref3;
+          return (_ref3 = _this[value]).active.apply(_ref3, arguments);
+        });
+        return _this.route(key, callback);
+      };
+      for (key in _ref2) {
+        value = _ref2[key];
+        _fn(key, value);
+      }
+      if (this["default"]) this[this["default"]].active();
+    }
+
+    Stack.prototype.add = function(controller) {
+      this.manager.add(controller);
+      return this.append(controller);
+    };
+
+    return Stack;
+
+  })(Spine.Controller);
+
+  if (typeof module !== "undefined" && module !== null) {
+    module.exports = Spine.Manager;
+  }
+
+}).call(this);

data/strelka-admin/static/js/vendor/spine/relation.js

+(function() {
+  var Collection, Instance, Singleton, isArray, singularize, underscore,
+    __hasProp = Object.prototype.hasOwnProperty,
+    __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; };
+
+  if (typeof Spine === "undefined" || Spine === null) Spine = require('spine');
+
+  isArray = Spine.isArray;
+
+  if (typeof require === "undefined" || require === null) {
+    require = (function(value) {
+      return eval(value);
+    });
+  }
+
+  Collection = (function(_super) {
+
+    __extends(Collection, _super);
+
+    function Collection(options) {
+      var key, value;
+      if (options == null) options = {};
+      for (key in options) {
+        value = options[key];
+        this[key] = value;
+      }
+    }
+
+    Collection.prototype.all = function() {
+      var _this = this;
+      return this.model.select(function(rec) {
+        return _this.associated(rec);
+      });
+    };
+
+    Collection.prototype.first = function() {
+      return this.all()[0];
+    };
+
+    Collection.prototype.last = function() {
+      var values;
+      values = this.all();
+      return values[values.length - 1];
+    };
+
+    Collection.prototype.find = function(id) {
+      var records,
+        _this = this;
+      records = this.select(function(rec) {
+        return rec.id + '' === id + '';
+      });
+      if (!records[0]) throw 'Unknown record';
+      return records[0];
+    };
+
+    Collection.prototype.findAllByAttribute = function(name, value) {
+      var _this = this;
+      return this.model.select(function(rec) {
+        return rec[name] === value;
+      });
+    };
+
+    Collection.prototype.findByAttribute = function(name, value) {
+      return this.findAllByAttribute(name, value)[0];
+    };
+
+    Collection.prototype.select = function(cb) {
+      var _this = this;
+      return this.model.select(function(rec) {
+        return _this.associated(rec) && cb(rec);
+      });
+    };
+
+    Collection.prototype.refresh = function(values) {
+      var record, records, _i, _j, _len, _len2, _ref;
+      _ref = this.all();
+      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+        record = _ref[_i];
+        delete this.model.records[record.id];
+      }
+      records = this.model.fromJSON(values);
+      if (!isArray(records)) records = [records];
+      for (_j = 0, _len2 = records.length; _j < _len2; _j++) {
+        record = records[_j];
+        record.newRecord = false;
+        record[this.fkey] = this.record.id;
+        this.model.records[record.id] = record;
+      }
+      return this.model.trigger('refresh', records);
+    };
+
+    Collection.prototype.create = function(record) {
+      record[this.fkey] = this.record.id;
+      return this.model.create(record);
+    };
+
+    Collection.prototype.associated = function(record) {
+      return record[this.fkey] === this.record.id;
+    };
+
+    return Collection;
+
+  })(Spine.Module);
+
+  Instance = (function(_super) {
+
+    __extends(Instance, _super);
+
+    function Instance(options) {
+      var key, value;
+      if (options == null) options = {};
+      for (key in options) {
+        value = options[key];
+        this[key] = value;
+      }
+    }
+
+    Instance.prototype.exists = function() {
+      return this.record[this.fkey] && this.model.exists(this.record[this.fkey]);
+    };
+
+    Instance.prototype.update = function(value) {
+      if (!(value instanceof this.model)) value = new this.model(value);
+      if (value.isNew()) value.save();
+      return this.record[this.fkey] = value && value.id;
+    };
+
+    return Instance;
+
+  })(Spine.Module);
+
+  Singleton = (function(_super) {
+
+    __extends(Singleton, _super);
+
+    function Singleton(options) {
+      var key, value;
+      if (options == null) options = {};
+      for (key in options) {
+        value = options[key];
+        this[key] = value;
+      }
+    }
+
+    Singleton.prototype.find = function() {
+      return this.record.id && this.model.findByAttribute(this.fkey, this.record.id);
+    };
+
+    Singleton.prototype.update = function(value) {
+      if (!(value instanceof this.model)) value = this.model.fromJSON(value);
+      value[this.fkey] = this.record.id;
+      return value.save();
+    };
+
+    return Singleton;
+
+  })(Spine.Module);
+
+  singularize = function(str) {
+    return str.replace(/s$/, '');
+  };
+
+  underscore = function(str) {
+    return str.replace(/::/g, '/').replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2').replace(/([a-z\d])([A-Z])/g, '$1_$2').replace(/-/g, '_').toLowerCase();
+  };
+
+  Spine.Model.extend({
+    hasMany: function(name, model, fkey) {
+      var association;
+      if (fkey == null) fkey = "" + (underscore(this.className)) + "_id";
+      association = function(record) {
+        if (typeof model === 'string') model = require(model);
+        return new Collection({
+          name: name,
+          model: model,
+          record: record,
+          fkey: fkey
+        });
+      };
+      return this.prototype[name] = function(value) {
+        if (value != null) association(this).refresh(value);
+        return association(this);
+      };
+    },
+    belongsTo: function(name, model, fkey) {
+      var association;
+      if (fkey == null) fkey = "" + (singularize(name)) + "_id";
+      association = function(record) {
+        if (typeof model === 'string') model = require(model);
+        return new Instance({
+          name: name,
+          model: model,
+          record: record,
+          fkey: fkey
+        });
+      };
+      this.prototype[name] = function(value) {
+        if (value != null) association(this).update(value);
+        return association(this).exists();
+      };
+      return this.attributes.push(fkey);
+    },
+    hasOne: function(name, model, fkey) {
+      var association;
+      if (fkey == null) fkey = "" + (underscore(this.className)) + "_id";
+      association = function(record) {
+        if (typeof model === 'string') model = require(model);
+        return new Singleton({
+          name: name,
+          model: model,
+          record: record,
+          fkey: fkey
+        });
+      };
+      return this.prototype[name] = function(value) {
+        if (value != null) association(this).update(value);
+        return association(this).find();
+      };
+    }
+  });
+
+}).call(this);

data/strelka-admin/static/js/vendor/spine/route.js

+(function() {
+  var $, escapeRegExp, hashStrip, namedParam, splatParam,
+    __hasProp = Object.prototype.hasOwnProperty,
+    __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; },
+    __slice = Array.prototype.slice;
+
+  if (typeof Spine === "undefined" || Spine === null) Spine = require('spine');
+
+  $ = Spine.$;
+
+  hashStrip = /^#*/;
+
+  namedParam = /:([\w\d]+)/g;
+
+  splatParam = /\*([\w\d]+)/g;
+
+  escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
+
+  Spine.Route = (function(_super) {
+    var _ref;
+
+    __extends(Route, _super);
+
+    Route.extend(Spine.Events);
+
+    Route.historySupport = ((_ref = window.history) != null ? _ref.pushState : void 0) != null;
+
+    Route.routes = [];
+
+    Route.options = {
+      trigger: true,
+      history: false,
+      shim: false
+    };
+
+    Route.add = function(path, callback) {
+      var key, value, _results;
+      if (typeof path === 'object' && !(path instanceof RegExp)) {
+        _results = [];
+        for (key in path) {
+          value = path[key];
+          _results.push(this.add(key, value));
+        }
+        return _results;
+      } else {
+        return this.routes.push(new this(path, callback));
+      }
+    };
+
+    Route.setup = function(options) {
+      if (options == null) options = {};
+      this.options = $.extend({}, this.options, options);
+      if (this.options.history) {
+        this.history = this.historySupport && this.options.history;
+      }
+      if (this.options.shim) return;
+      if (this.history) {
+        $(window).bind('popstate', this.change);
+      } else {
+        $(window).bind('hashchange', this.change);
+      }
+      return this.change();
+    };
+
+    Route.unbind = function() {
+      if (this.history) {
+        return $(window).unbind('popstate', this.change);
+      } else {
+        return $(window).unbind('hashchange', this.change);
+      }
+    };
+
+    Route.navigate = function() {
+      var args, lastArg, options, path;
+      args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+      options = {};
+      lastArg = args[args.length - 1];
+      if (typeof lastArg === 'object') {
+        options = args.pop();
+      } else if (typeof lastArg === 'boolean') {
+        options.trigger = args.pop();
+      }
+      options = $.extend({}, this.options, options);
+      path = args.join('/');
+      if (this.path === path) return;
+      this.path = path;
+      this.trigger('navigate', this.path);
+      if (options.trigger) this.matchRoute(this.path, options);
+      if (options.shim) return;
+      if (this.history) {
+        return history.pushState({}, document.title, this.path);
+      } else {
+        return window.location.hash = this.path;
+      }
+    };
+
+    Route.getPath = function() {
+      var path;
+      path = window.location.pathname;
+      if (path.substr(0, 1) !== '/') path = '/' + path;
+      return path;
+    };
+
+    Route.getHash = function() {
+      return window.location.hash;
+    };
+
+    Route.getFragment = function() {
+      return this.getHash().replace(hashStrip, '');
+    };
+
+    Route.getHost = function() {
+      return (document.location + '').replace(this.getPath() + this.getHash(), '');
+    };
+
+    Route.change = function() {
+      var path;
+      path = this.getFragment() !== '' ? this.getFragment() : this.getPath();
+      if (path === this.path) return;
+      this.path = path;
+      return this.matchRoute(this.path);
+    };
+
+    Route.matchRoute = function(path, options) {
+      var route, _i, _len, _ref2;
+      _ref2 = this.routes;
+      for (_i = 0, _len = _ref2.length; _i < _len; _i++) {
+        route = _ref2[_i];
+        if (route.match(path, options)) {
+          this.trigger('change', route, path);
+          return route;
+        }
+      }
+    };
+
+    function Route(path, callback) {
+      var match;
+      this.path = path;
+      this.callback = callback;
+      this.names = [];
+      if (typeof path === 'string') {
+        namedParam.lastIndex = 0;
+        while ((match = namedParam.exec(path)) !== null) {
+          this.names.push(match[1]);
+        }
+        path = path.replace(escapeRegExp, '\\$&').replace(namedParam, '([^\/]*)').replace(splatParam, '(.*?)');
+        this.route = new RegExp('^' + path + '$');
+      } else {
+        this.route = path;
+      }
+    }
+
+    Route.prototype.match = function(path, options) {
+      var i, match, param, params, _len;
+      if (options == null) options = {};
+      match = this.route.exec(path);
+      if (!match) return false;
+      options.match = match;
+      params = match.slice(1);
+      if (this.names.length) {
+        for (i = 0, _len = params.length; i < _len; i++) {
+          param = params[i];
+          options[this.names[i]] = param;
+        }
+      }
+      return this.callback.call(null, options) !== false;
+    };
+
+    return Route;
+
+  })(Spine.Module);
+
+  Spine.Route.change = Spine.Route.proxy(Spine.Route.change);
+
+  Spine.Controller.include({
+    route: function(path, callback) {
+      return Spine.Route.add(path, this.proxy(callback));
+    },
+    routes: function(routes) {
+      var key, value, _results;
+      _results = [];
+      for (key in routes) {
+        value = routes[key];
+        _results.push(this.route(key, value));
+      }
+      return _results;
+    },
+    navigate: function() {
+      return Spine.Route.navigate.apply(Spine.Route, arguments);
+    }
+  });
+
+  if (typeof module !== "undefined" && module !== null) {
+    module.exports = Spine.Route;
+  }
+
+}).call(this);

data/strelka-admin/static/js/vendor/spine/spine.js

+(function() {
+  var $, Controller, Events, Log, Model, Module, Spine, isArray, isBlank, makeArray, moduleKeywords,
+    __slice = Array.prototype.slice,
+    __indexOf = Array.prototype.indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
+    __hasProp = Object.prototype.hasOwnProperty,
+    __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; },
+    __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+  Events = {
+    bind: function(ev, callback) {
+      var calls, evs, name, _i, _len;
+      evs = ev.split(' ');
+      calls = this.hasOwnProperty('_callbacks') && this._callbacks || (this._callbacks = {});
+      for (_i = 0, _len = evs.length; _i < _len; _i++) {
+        name = evs[_i];
+        calls[name] || (calls[name] = []);
+        calls[name].push(callback);
+      }
+      return this;
+    },
+    one: function(ev, callback) {
+      return this.bind(ev, function() {
+        this.unbind(ev, arguments.callee);
+        return callback.apply(this, arguments);
+      });
+    },
+    trigger: function() {
+      var args, callback, ev, list, _i, _len, _ref;
+      args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+      ev = args.shift();
+      list = this.hasOwnProperty('_callbacks') && ((_ref = this._callbacks) != null ? _ref[ev] : void 0);
+      if (!list) return;
+      for (_i = 0, _len = list.length; _i < _len; _i++) {
+        callback = list[_i];
+        if (callback.apply(this, args) === false) break;
+      }
+      return true;
+    },
+    unbind: function(ev, callback) {