You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@netbeans.apache.org by ge...@apache.org on 2019/05/30 14:56:23 UTC

[netbeans-tools] branch master updated: adding Synergy sources, from 3rd donation

This is an automated email from the ASF dual-hosted git repository.

geertjan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/netbeans-tools.git


The following commit(s) were added to refs/heads/master by this push:
     new c34a844  adding Synergy sources, from 3rd donation
c34a844 is described below

commit c34a844fb9b3c8f2ffaece85c1ffc5e78033bd7d
Author: Geertjan Wielenga <ge...@apache.org>
AuthorDate: Thu May 30 16:56:10 2019 +0200

    adding Synergy sources, from 3rd donation
---
 .DS_Store                                          |  Bin 0 -> 8196 bytes
 synergy/Gruntfile.js                               |  156 +
 synergy/README.md                                  |   61 +
 synergy/build.xml                                  |   87 +
 synergy/client/app/admin.html                      |  134 +
 synergy/client/app/css/custom.css                  |  477 ++
 synergy/client/app/css/docs.css                    | 1001 +++
 synergy/client/app/css/min/custom.css              |    1 +
 synergy/client/app/css/min/docs.css                |    1 +
 synergy/client/app/favicon.ico                     |  Bin 0 -> 1150 bytes
 synergy/client/app/img/ajax-loader.gif             |  Bin 0 -> 3169 bytes
 synergy/client/app/img/blue.png                    |  Bin 0 -> 1731 bytes
 .../client/app/img/bs-docs-bootstrap-features.png  |  Bin 0 -> 5872 bytes
 .../client/app/img/bs-docs-masthead-pattern.png    |  Bin 0 -> 14189 bytes
 synergy/client/app/img/clock.png                   |  Bin 0 -> 1558 bytes
 .../client/app/img/glyphicons-halflings-white.png  |  Bin 0 -> 12371 bytes
 synergy/client/app/img/glyphicons-halflings.png    |  Bin 0 -> 17141 bytes
 synergy/client/app/img/grey.png                    |  Bin 0 -> 1693 bytes
 synergy/client/app/img/grid-baseline-20px.png      |  Bin 0 -> 102 bytes
 synergy/client/app/img/nb.gif                      |  Bin 0 -> 2207 bytes
 synergy/client/app/img/red.png                     |  Bin 0 -> 1665 bytes
 synergy/client/app/img/user.png                    |  Bin 0 -> 691 bytes
 synergy/client/app/img/yellow.png                  |  Bin 0 -> 1730 bytes
 synergy/client/app/index.html                      |  149 +
 synergy/client/app/index2.html                     |  147 +
 synergy/client/app/index_dev.html                  |  179 +
 synergy/client/app/js/app.js                       |  260 +
 synergy/client/app/js/configuration.js             |  393 ++
 synergy/client/app/js/controllers.js               | 7283 ++++++++++++++++++++
 synergy/client/app/js/excl/inspect.js              |  169 +
 synergy/client/app/js/excl/inspectx.js             |  169 +
 synergy/client/app/js/excl/wgxpath.install.js      |   49 +
 synergy/client/app/js/exts.js                      |    7 +
 synergy/client/app/js/factories.js                 | 3332 +++++++++
 synergy/client/app/js/filters.js                   |   37 +
 synergy/client/app/js/handlers.js                  |  749 ++
 synergy/client/app/js/legacy/polyfills.js          |   77 +
 synergy/client/app/js/login/app.js                 |   29 +
 synergy/client/app/js/min/synergy.js               |    3 +
 synergy/client/app/js/models.js                    |  296 +
 synergy/client/app/js/utils.js                     |  322 +
 synergy/client/app/login.html                      |   84 +
 synergy/client/app/opensearch.xml                  |   11 +
 .../app/partials/admin/create/assignment.html      |   58 +
 .../partials/admin/create/matrix_assignment.html   |   72 +
 synergy/client/app/partials/admin/create/run.html  |   65 +
 .../client/app/partials/admin/create/tribe.html    |   58 +
 synergy/client/app/partials/admin/create/user.html |  132 +
 .../client/app/partials/admin/edit/project.html    |   74 +
 synergy/client/app/partials/admin/edit/run.html    |  111 +
 synergy/client/app/partials/admin/edit/user.html   |  130 +
 .../client/app/partials/admin/view/database.html   |   39 +
 synergy/client/app/partials/admin/view/home.html   |    7 +
 synergy/client/app/partials/admin/view/log.html    |   14 +
 .../client/app/partials/admin/view/platforms.html  |   70 +
 .../client/app/partials/admin/view/projects.html   |   49 +
 .../client/app/partials/admin/view/reviews.html    |   61 +
 synergy/client/app/partials/admin/view/runs.html   |   64 +
 .../client/app/partials/admin/view/settings.html   |   39 +
 synergy/client/app/partials/admin/view/tribes.html |   42 +
 synergy/client/app/partials/admin/view/users.html  |   60 +
 .../client/app/partials/admin/view/versions.html   |   69 +
 .../client/app/partials/directives/loginBt.html    |   17 +
 .../app/partials/public/create/assignment.html     |   71 +
 .../partials/public/create/assignment_tribe.html   |   73 +
 .../client/app/partials/public/create/case.html    |   63 +
 .../app/partials/public/create/specification.html  |   46 +
 .../client/app/partials/public/create/suite.html   |   61 +
 synergy/client/app/partials/public/edit/case.html  |  149 +
 .../client/app/partials/public/edit/review.html    |   66 +
 .../app/partials/public/edit/specification.html    |  150 +
 synergy/client/app/partials/public/edit/suite.html |   69 +
 synergy/client/app/partials/public/edit/tribe.html |   70 +
 synergy/client/app/partials/public/login/home.html |   11 +
 synergy/client/app/partials/public/view/about.html |    9 +
 .../app/partials/public/view/assignment.html       |  115 +
 .../partials/public/view/assignment_comments.html  |   64 +
 .../client/app/partials/public/view/calendar.html  |   14 +
 synergy/client/app/partials/public/view/case.html  |  116 +
 .../client/app/partials/public/view/favorites.html |   20 +
 synergy/client/app/partials/public/view/home.html  |   57 +
 synergy/client/app/partials/public/view/label.html |   17 +
 synergy/client/app/partials/public/view/login.html |   28 +
 .../client/app/partials/public/view/profile.html   |  175 +
 .../client/app/partials/public/view/recover.html   |   17 +
 .../client/app/partials/public/view/register.html  |   52 +
 .../client/app/partials/public/view/review.html    |   26 +
 .../client/app/partials/public/view/revisions.html |   46 +
 .../app/partials/public/view/run_coverage.html     |   57 +
 .../app/partials/public/view/run_view_1.html       |  305 +
 .../app/partials/public/view/run_view_2.html       |  292 +
 .../app/partials/public/view/run_view_3.html       |   91 +
 synergy/client/app/partials/public/view/runs.html  |   55 +
 .../client/app/partials/public/view/search.html    |   17 +
 .../partials/public/view/specification_view_1.html |  142 +
 .../partials/public/view/specification_view_2.html |  228 +
 .../client/app/partials/public/view/specpool.html  |   32 +
 .../app/partials/public/view/statistics.html       |  215 +
 synergy/client/app/partials/public/view/suite.html |   96 +
 synergy/client/app/partials/public/view/tribe.html |   89 +
 .../client/app/partials/public/view/tribes.html    |   11 +
 synergy/client/config/testacular-e2e.conf.js       |   18 +
 synergy/client/config/testacular.conf.js           |   15 +
 synergy/client/scripts/e2e-test.bat                |   10 +
 synergy/client/scripts/e2e-test.sh                 |    9 +
 synergy/client/scripts/test-server.bat             |   19 +
 synergy/client/scripts/test-server.sh              |   14 +
 synergy/client/scripts/test.bat                    |   14 +
 synergy/client/scripts/test.sh                     |    9 +
 synergy/client/scripts/watchr.rb                   |   19 +
 synergy/client/scripts/web-server.js               |  243 +
 synergy/client/test/app/synergy.js                 |    9 +
 synergy/client/test/app/test.html                  |  143 +
 synergy/client/test/e2e/config.js                  |   41 +
 synergy/client/test/e2e/homeSpec.js                |   56 +
 synergy/client/test/e2e/runsSpec.js                |   91 +
 synergy/client/test/e2e/specificationsSpec.js      |   60 +
 synergy/manual/.htaccess                           |    5 +
 synergy/misc/database_schema/schema_inserts.sql    |  565 ++
 synergy/misc/migration                             |   50 +
 synergy/nbproject/customs.json                     |   27 +
 synergy/nbproject/project.properties               |   34 +
 synergy/nbproject/project.xml                      |    9 +
 synergy/package.json                               |   16 +
 synergy/server/api/.htaccess                       |   10 +
 synergy/server/api/_dummy2.php                     |   13 +
 synergy/server/api/about.php                       |    9 +
 synergy/server/api/assignment.php                  |  245 +
 synergy/server/api/assignment_bugs.php             |   66 +
 synergy/server/api/assignment_comments.php         |   38 +
 synergy/server/api/assignment_exists.php           |   22 +
 synergy/server/api/assignments.php                 |   65 +
 synergy/server/api/attachment.php                  |  103 +
 synergy/server/api/attachments.php                 |   33 +
 synergy/server/api/case.php                        |  138 +
 synergy/server/api/cases.php                       |   23 +
 synergy/server/api/comments.php                    |   11 +
 synergy/server/api/configuration.php               |   36 +
 synergy/server/api/db.php                          |   39 +
 synergy/server/api/events.php                      |   19 +
 synergy/server/api/favorite.php                    |   34 +
 synergy/server/api/favorites.php                   |   33 +
 synergy/server/api/image.php                       |   77 +
 synergy/server/api/images.php                      |   32 +
 synergy/server/api/import.php                      |   29 +
 synergy/server/api/issue.php                       |   70 +
 synergy/server/api/job.php                         |   50 +
 synergy/server/api/label.php                       |   88 +
 synergy/server/api/labels.php                      |   87 +
 synergy/server/api/log.php                         |   30 +
 synergy/server/api/login.php                       |   71 +
 synergy/server/api/platform.php                    |   80 +
 synergy/server/api/platforms.php                   |   49 +
 synergy/server/api/products.php                    |   16 +
 synergy/server/api/profile_img.php                 |   73 +
 synergy/server/api/project.php                     |   84 +
 synergy/server/api/projects.php                    |   22 +
 synergy/server/api/proxy.php                       |   36 +
 synergy/server/api/refresh.php                     |   41 +
 synergy/server/api/register.php                    |   42 +
 synergy/server/api/review.php                      |   39 +
 synergy/server/api/review_assignment.php           |  199 +
 synergy/server/api/reviews.php                     |   49 +
 synergy/server/api/revisions.php                   |   41 +
 synergy/server/api/revison.php                     |   13 +
 synergy/server/api/run.php                         |  186 +
 synergy/server/api/run_attachment.php              |   99 +
 synergy/server/api/run_notifications.php           |   29 +
 synergy/server/api/run_notifications_auto.php      |   30 +
 synergy/server/api/run_notifications_auto_test.php |   30 +
 synergy/server/api/run_specifications.php          |   37 +
 synergy/server/api/run_tribes.php                  |   46 +
 synergy/server/api/runs.php                        |   71 +
 synergy/server/api/sanitizer.php                   |   20 +
 synergy/server/api/search.php                      |   25 +
 synergy/server/api/specification.php               |  251 +
 synergy/server/api/specification_length.php        |   27 +
 synergy/server/api/specification_request.php       |   50 +
 synergy/server/api/specifications.php              |  113 +
 synergy/server/api/statistics.php                  |   38 +
 synergy/server/api/statistics_filter.php           |   39 +
 synergy/server/api/suite.php                       |  196 +
 synergy/server/api/test.php                        |   11 +
 synergy/server/api/tribe.php                       |  169 +
 synergy/server/api/tribe_assignments.php           |   44 +
 synergy/server/api/tribe_specification.php         |   53 +
 synergy/server/api/tribes.php                      |   69 +
 synergy/server/api/user.php                        |  150 +
 synergy/server/api/users.php                       |   69 +
 synergy/server/api/version.php                     |   69 +
 synergy/server/api/versions.json                   |  230 +
 synergy/server/api/versions.php                    |   36 +
 synergy/server/api/versions_dev.php                |   30 +
 synergy/server/api/versions_dev_1.php              |   42 +
 synergy/server/app/Synergy.php                     |  173 +
 synergy/server/cache/ical.ics                      |   15 +
 .../server/controller/AssignmentCommentsCtrl.php   |  145 +
 synergy/server/controller/AssignmentCtrl.php       |  171 +
 synergy/server/controller/AttachmentCtrl.php       |  142 +
 synergy/server/controller/CalendarCtrl.php         |  126 +
 synergy/server/controller/CaseCtrl.php             |  396 ++
 synergy/server/controller/CommentsCtrl.php         |   27 +
 synergy/server/controller/ConfigurationCtrl.php    |   47 +
 synergy/server/controller/DatabaseCtrl.php         |   55 +
 synergy/server/controller/ExtensionCtrl.php        |  185 +
 synergy/server/controller/LabelCtrl.php            |   40 +
 synergy/server/controller/Mediator.php             |   81 +
 synergy/server/controller/NotificationCtrl.php     |  267 +
 synergy/server/controller/PlatformCtrl.php         |   78 +
 synergy/server/controller/ProjectCtrl.php          |   74 +
 synergy/server/controller/RegistrationCtrl.php     |   57 +
 synergy/server/controller/ReviewCtrl.php           |  197 +
 synergy/server/controller/ReviewsCtrl.php          |   61 +
 synergy/server/controller/RevisionCtrl.php         |  112 +
 synergy/server/controller/RunCtrl.php              | 1078 +++
 synergy/server/controller/RunNotificationCtrl.php  |  235 +
 synergy/server/controller/SearchCtrl.php           |   70 +
 synergy/server/controller/SessionRefreshCtrl.php   |   32 +
 synergy/server/controller/SpecRelationCtrl.php     |  104 +
 synergy/server/controller/SpecificationCtrl.php    |  572 ++
 .../server/controller/SpecificationLockCtrl.php    |  100 +
 synergy/server/controller/StatisticsCtrl.php       |  115 +
 synergy/server/controller/SuiteCtrl.php            |  182 +
 synergy/server/controller/TribeCtrl.php            |  449 ++
 synergy/server/controller/UserCtrl.php             |  409 ++
 synergy/server/controller/VersionCtrl.php          |  102 +
 synergy/server/data/sample.json                    |    1 +
 synergy/server/db/AssignmentCommentsDAO.php        |  107 +
 synergy/server/db/AssignmentDAO.php                |  130 +
 synergy/server/db/AttachmentDAO.php                |  215 +
 synergy/server/db/Bugzilla_DAO.php                 |   55 +
 synergy/server/db/CaseDAO.php                      |  661 ++
 synergy/server/db/CiDAO.php                        |   99 +
 synergy/server/db/CommentsDAO.php                  |   28 +
 synergy/server/db/ConfigurationDAO.php             |   80 +
 synergy/server/db/DB_DAO.php                       |  122 +
 synergy/server/db/IssueDAO.php                     |   96 +
 synergy/server/db/LabelDAO.php                     |   59 +
 synergy/server/db/LockDAO.php                      |  105 +
 synergy/server/db/PlatformDAO.php                  |  142 +
 synergy/server/db/ProductDAO.php                   |   42 +
 synergy/server/db/ProjectDAO.php                   |  239 +
 synergy/server/db/RegistrationDAO.php              |   35 +
 synergy/server/db/RemovalDAO.php                   |   71 +
 synergy/server/db/ReviewDAO.php                    |  388 ++
 synergy/server/db/ReviewsDAO.php                   |   93 +
 synergy/server/db/RevisionDAO.php                  |   72 +
 synergy/server/db/RunDAO.php                       | 1062 +++
 synergy/server/db/RunNotificationDAO.php           |   77 +
 synergy/server/db/SessionDAO.php                   |  114 +
 synergy/server/db/SessionRefreshDAO.php            |   59 +
 synergy/server/db/SpecificationDAO.php             |  816 +++
 synergy/server/db/SpecificationRelationDAO.php     |   30 +
 synergy/server/db/StructureDAO.php                 |   54 +
 synergy/server/db/SuiteDAO.php                     |  383 +
 synergy/server/db/TribeDAO.php                     |  398 ++
 synergy/server/db/TribeExtensionDAO.php            |   98 +
 synergy/server/db/UserDAO.php                      |  515 ++
 synergy/server/db/VersionDAO.php                   |  179 +
 synergy/server/db/structure.sql                    |  470 ++
 synergy/server/errors/.htaccess                    |    9 +
 synergy/server/errors/errors.log                   |    0
 .../ContinuousIntegrationExtension.php             |   68 +
 .../extensions/specification/ProjectExtension.php  |   75 +
 .../specification/RemovalRequestExtension.php      |   62 +
 .../extensions/specification/TestExtension.php     |   42 +
 .../server/extensions/suite/ProjectExtension.php   |   57 +
 .../extensions/testcase/ProjectExtension.php       |   57 +
 .../tribe/TribeSpecificationExtension.php          |   78 +
 synergy/server/interfaces/EmailProvider.php        |   28 +
 synergy/server/interfaces/ExtensionInterface.php   |   48 +
 synergy/server/interfaces/IssueProvider.php        |   21 +
 synergy/server/interfaces/LoggerProvider.php       |   29 +
 synergy/server/interfaces/Observer.php             |   22 +
 synergy/server/interfaces/ReviewImporter.php       |   17 +
 synergy/server/interfaces/SessionProvider.php      |   76 +
 synergy/server/interfaces/TutorialProvider.php     |   19 +
 synergy/server/misc/HTTP.php                       |  129 +
 synergy/server/misc/Util.php                       |  202 +
 synergy/server/model/Action.php                    |   32 +
 synergy/server/model/AssignmentComment.php         |   64 +
 synergy/server/model/AssignmentComments.php        |   23 +
 synergy/server/model/AssignmentDuration.php        |   20 +
 synergy/server/model/AssignmentProgress.php        |   32 +
 synergy/server/model/BlobSpecification.php         |   47 +
 synergy/server/model/BlobTestCase.php              |   33 +
 synergy/server/model/BlobTestSuite.php             |   45 +
 synergy/server/model/Bug.php                       |   52 +
 synergy/server/model/CachedSession.php             |   21 +
 synergy/server/model/CommentType.php               |   20 +
 synergy/server/model/CurlRequestResult.php         |   18 +
 synergy/server/model/Email.php                     |   28 +
 synergy/server/model/Job.php                       |   34 +
 synergy/server/model/Label.php                     |   26 +
 synergy/server/model/LabelResult.php               |   36 +
 synergy/server/model/Membership.php                |   27 +
 synergy/server/model/Platform.php                  |   71 +
 synergy/server/model/Product.php                   |   36 +
 synergy/server/model/Revision.php                  |   28 +
 synergy/server/model/RunAttachment.php             |   55 +
 synergy/server/model/SearchResult.php              |   33 +
 synergy/server/model/Session.php                   |   67 +
 synergy/server/model/Setting.php                   |   48 +
 synergy/server/model/Specification.php             |  243 +
 synergy/server/model/SpecificationAttachment.php   |   69 +
 synergy/server/model/SpecificationListItem.php     |   54 +
 synergy/server/model/SpecificationSkeleton.php     |   26 +
 .../server/model/SpecificationsSimpleNameList.php  |   80 +
 synergy/server/model/StatRecord.php                |   25 +
 synergy/server/model/Suite.php                     |  176 +
 synergy/server/model/SuiteSkeleton.php             |   27 +
 synergy/server/model/TestAssignment.php            |  197 +
 synergy/server/model/TestCase.php                  |  155 +
 synergy/server/model/TestCaseImage.php             |   88 +
 synergy/server/model/TestCaseSkeleton.php          |   31 +
 synergy/server/model/TestRun.php                   |  117 +
 synergy/server/model/TestRunList.php               |   34 +
 synergy/server/model/TestRunStatistics.php         |   30 +
 synergy/server/model/Tribe.php                     |   98 +
 synergy/server/model/User.php                      |   94 +
 synergy/server/model/UserStatistics.php            |   86 +
 synergy/server/model/UsersResult.php               |   41 +
 synergy/server/model/Version.php                   |  110 +
 .../assignment/rest/AssignmentLineResource.php     |   36 +
 .../assignment/rest/AssignmentListItemResource.php |   73 +
 .../rest/AssignmentStatisticsResource.php          |   40 +
 .../rest/RichAssignmentListItemResource.php        |   43 +
 synergy/server/model/bug/rest/BugResource.php      |   44 +
 .../server/model/comment/rest/CommentResource.php  |   56 +
 .../model/comment/rest/CommentTypeResource.php     |   30 +
 .../model/comment/rest/CommentsListResource.php    |   24 +
 .../model/exception/AssignmentCommentException.php |   25 +
 .../exception/AssignmentConflictException.php      |   25 +
 .../server/model/exception/AssignmentException.php |   26 +
 .../exception/AssignmentSecurityException.php      |   26 +
 .../exception/CorruptedAssignmentException.php     |   25 +
 .../model/exception/CurlRequestException.php       |   26 +
 .../server/model/exception/GeneralException.php    |   31 +
 .../exception/SpecificationDuplicateException.php  |   26 +
 synergy/server/model/exception/UserException.php   |   32 +
 synergy/server/model/image/rest/ImageResource.php  |   38 +
 synergy/server/model/label/rest/LabelResource.php  |   30 +
 .../model/label/rest/LabelSearchResource.php       |   31 +
 .../model/platform/rest/PlatformResource.php       |   34 +
 .../server/model/product/rest/ProductResource.php  |   30 +
 synergy/server/model/project/Project.php           |   80 +
 synergy/server/model/project/ProjectListItem.php   |   20 +
 .../server/model/project/rest/ProjectResource.php  |   39 +
 synergy/server/model/registration/Registration.php |   25 +
 synergy/server/model/review/ReviewAssignment.php   |  140 +
 synergy/server/model/review/ReviewComment.php      |   50 +
 synergy/server/model/review/ReviewPage.php         |   45 +
 .../model/review/rest/ReviewStatisticsResource.php |   77 +
 .../revision/rest/RevisionListItemResource.php     |   32 +
 .../model/revision/rest/RevisionResource.php       |   34 +
 synergy/server/model/run/RunNotification.php       |   93 +
 .../model/run/rest/RunAttachmentResource.php       |   36 +
 synergy/server/model/run/rest/RunBlobsResource.php |   41 +
 .../server/model/run/rest/RunListItemResource.php  |   53 +
 synergy/server/model/run/rest/RunResource.php      |   42 +
 .../run/rest/RunSpecificationsListResource.php     |   23 +
 .../model/run/rest/RunStatisticsResource.php       |   37 +
 .../model/search/rest/SearchResultResource.php     |   17 +
 synergy/server/model/session/RefreshSession.php    |   20 +
 .../server/model/setting/rest/SettingResource.php  |   32 +
 .../model/specification/ext/RemovalRequest.php     |   22 +
 .../rest/SpecificationAttachmentResource.php       |   34 +
 .../rest/SpecificationListItemResource.php         |   44 +
 .../specification/rest/SpecificationResource.php   |   74 +
 .../statistics/rest/StatisticsLineResource.php     |   34 +
 synergy/server/model/suite/rest/SuiteResource.php  |   41 +
 .../model/suite/rest/SuiteSnippetResource.php      |   57 +
 .../model/testcase/rest/CaseListItemResource.php   |   34 +
 .../server/model/testcase/rest/CaseResource.php    |   44 +
 .../model/testcase/rest/CaseSnippetResource.php    |   56 +
 .../model/tribe/rest/TribeListItemResource.php     |   40 +
 synergy/server/model/tribe/rest/TribeResource.php  |   48 +
 .../server/model/user/rest/MembershipResource.php  |   32 +
 .../model/user/rest/UserListItemResource.php       |   38 +
 synergy/server/model/user/rest/UserResource.php    |   63 +
 .../server/model/version/rest/VersionResource.php  |   34 +
 synergy/server/observer/SpecificationObserver.php  |   53 +
 synergy/server/providers/EmailCtrl.php             |   39 +
 synergy/server/providers/IssueCtrl.php             |   70 +
 synergy/server/providers/IssueOtherCtrl.php        |   61 +
 synergy/server/providers/LoggerCtrl.php            |   38 +
 synergy/server/providers/ProductCtrl.php           |   36 +
 synergy/server/providers/ReviewImporterCtrl.php    |   77 +
 synergy/server/providers/SessionCtrl.php           |  134 +
 .../server/providers/SessionCtrl_Production.php    |  224 +
 synergy/server/providers/SessionCtrl_SSO.php       |  153 +
 synergy/server/providers/TutorialFormatter.php     |   44 +
 synergy/server/setup/conf.php                      |  134 +
 synergy/server_tests/bootstrap.php                 |   81 +
 synergy/server_tests/configuration.xml             |    8 +
 synergy/server_tests/server/DatabaseSetup.php      |   88 +
 .../server/controller/AttachmentCtrlTest.php       |   47 +
 .../server/controller/CaseCtrlTest.php             |  237 +
 .../server/controller/CaseExtensionCtrlTest.php    |   51 +
 .../server/controller/LabelCtrlTest.php            |   43 +
 .../server/controller/PlatformCtrlTest.php         |   66 +
 .../server_tests/server/controller/RunCtrlTest.php |  288 +
 .../server/controller/SearchCtrlTest.php           |   46 +
 .../server/controller/SpecificationCtrlTest.php    |  187 +
 .../controller/SpecificationExtensionCtrlTest.php  |   37 +
 .../server/controller/StatisticsCtrlTest.php       |   34 +
 .../server/controller/SuiteCtrlTest.php            |  137 +
 .../server/controller/SuiteExtensionCtrlTest.php   |   52 +
 .../server/controller/TribeCtrlTest.php            |  106 +
 .../server/controller/TribeExtensionCtrlTest.php   |   48 +
 .../server/controller/UserCtrlTest.php             |  187 +
 .../server/controller/VersionCtrlTest.php          |   83 +
 synergy/server_tests/server/db/FixtureTestCase.php |  115 +
 .../server/db/SpecificationDAOTest.php             |   35 +
 synergy/server_tests/server/db/fixtures/dump.xml   | 1315 ++++
 .../server/db/fixtures/specification.xml           |  120 +
 synergy/synergy.wiki/.htaccess                     |    5 +
 417 files changed, 49668 insertions(+)

diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..0d9c46e
Binary files /dev/null and b/.DS_Store differ
diff --git a/synergy/Gruntfile.js b/synergy/Gruntfile.js
new file mode 100755
index 0000000..ee3c46d
--- /dev/null
+++ b/synergy/Gruntfile.js
@@ -0,0 +1,156 @@
+var execSync = require("child_process").execSync;
+var svnRevision = execSync("svn info -r 'HEAD' | grep Revision: | awk -F' ' '{print $2}'").toString();
+svnRevision = svnRevision.replace(/\s/g, "");
+module.exports = function (grunt) {
+    var timestamp = Date.now();
+    grunt.initConfig({
+        pkg: grunt.file.readJSON('package.json'),
+        replace: {
+            index: {
+                src: ['client/app/index.html'],
+                dest: ['client/app/index.html'],
+                replacements: [{
+                        from: /synergy\.js\?v=[0-9]+"/,
+                        to: 'synergy.js?v=' + timestamp + "\""
+                    }, {
+                        from: /polyfills\.js\?v=[0-9]+"/,
+                        to: 'polyfills.js?v=' + timestamp + "\""
+                    }]
+            },
+            version: {
+                src: ['client/app/js/configuration.js'],
+                dest: ['client/app/js/configuration.js'],
+                replacements: [{
+                        from: /this\.version = "1\.0\.[0-9]+";/,
+                        to: 'this.version = "1.0.' + svnRevision + '";'
+                    }]
+            },
+            index2: {
+                src: ['client/app/index2.html'],
+                dest: ['client/app/index2.html'],
+                replacements: [{
+                        from: /synergy\.js\?v=[0-9]+"/,
+                        to: 'synergy.js?v=' + timestamp + "\""
+                    }, {
+                        from: /polyfills\.js\?v=[0-9]+"/,
+                        to: 'polyfills.js?v=' + timestamp + "\""
+                    }]
+            },
+            appjs: {
+                src: ['client/app/js/app.js'],
+                dest: ['client/app/js/app.js'],
+                replacements: [{
+                        from: /\.html\?v=[0-9]+/g,
+                        to: '.html?v=' + timestamp
+                    }]
+            },
+            css: {
+                src: ['client/app/index.html'],
+                dest: ['client/app/index.html'],
+                replacements: [{//custom.css?v=7
+                        from: /custom\.css\?v=[0-9]+"/,
+                        to: 'custom.css?v=' + timestamp + "\""
+                    }]
+            },
+            css2: {
+                src: ['client/app/index2.html'],
+                dest: ['client/app/index2.html'],
+                replacements: [{//custom.css?v=7
+                        from: /custom\.css\?v=[0-9]+"/,
+                        to: 'custom.css?v=' + timestamp + "\""
+                    }]
+            },
+            testSynergyPartials: {
+                src: ['client/test/app/synergy.js'],
+                dest: ['client/test/app/synergy.js'],
+                replacements: [{//custom.css?v=7
+                        from: /partials\//g,
+                        to: '../../app/partials/'
+                    }]
+            },
+            testSynergyResources: {
+                src: ['client/test/app/synergy.js'],
+                dest: ['client/test/app/synergy.js'],
+                replacements: [{//custom.css?v=7
+                        from: /\.\.\/\.\.\/server\/api/g,
+                        to: '../../../server/api'
+                    }]
+            },
+            testReplaceDatabaseName: {
+                src: ['server/setup/conf.php'],
+                dest: ['server/setup/conf.php'],
+                replacements: [{//custom.css?v=7
+                        from: /define\('DHOST', 'mysql:host=localhost;dbname=synergy;charset=UTF8'\);/g,
+                        to: 'define(\'DHOST\', \'mysql:host=localhost;dbname=synergy_test;charset=UTF8\');'
+                    }]
+            },
+            replaceDatabaseName: {
+                src: ['server/setup/conf.php'],
+                dest: ['server/setup/conf.php'],
+                replacements: [{//custom.css?v=7
+                        from: /define\('DHOST', 'mysql:host=localhost;dbname=synergy_test;charset=UTF8'\);/g,
+                        to: 'define(\'DHOST\', \'mysql:host=localhost;dbname=synergy;charset=UTF8\');'
+                    }]
+            }
+        },
+        cssmin: {
+            combine: {
+                files: {
+                    'client/app/css/min/custom.css': ['client/app/css/custom.css'],
+                    'client/app/css/min/docs.css': ['client/app/css/docs.css'],
+                    'client/app/css/min/bootstrap.css': ['client/app/css/bootstrap.css'],
+                    'client/app/css/min/bootstrap-responsive.css': ['client/app/css/bootstrap-responsive.css']
+                }
+            }
+        },
+        uglify: {
+            options: {
+                mangle: false,
+                banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
+            },
+            buildSynergy: {
+                files: {
+                    'client/app/js/min/synergy.js': ['client/app/js/*.js']
+                }
+            },
+            buildTestSynergy: {
+                files: {
+                    'client/test/app/synergy.js': ['client/app/js/min/synergy.js']
+                }
+            }
+        },
+        jshint: {
+            "client": {
+                "src": ["client/app/js/*.js"],
+                options: {
+                    "reporterOutput": "",
+                    "force": true,
+                    "strict": true,
+                    "curly": true,
+                    "eqnull": true,
+//                    "unused": true,
+                    "eqeqeq": true,
+                    "undef": true,
+//                    "camelcase": true,
+                    "forin": true,
+                    "immed": true,
+                    "latedef": true,
+                    "newcap": true,
+                    "expr": true,
+                    "quotmark": "double",
+                    "trailing": true,
+//                 "globalstrict": true,//
+                    globals: {difflib: true, diffview: true, "$": true, angular: true, window: true, google: true},
+                    reporter: require('jshint-stylish'),
+                    '-W097': true // use strict in function form warning
+                }
+            }
+        }
+    });
+    grunt.loadNpmTasks('grunt-contrib-uglify');
+    grunt.loadNpmTasks('grunt-contrib-cssmin');
+    grunt.loadNpmTasks('grunt-contrib-jshint');
+    grunt.loadNpmTasks('grunt-text-replace');
+    grunt.registerTask('default', ['jshint', 'replace:index', 'replace:version', 'replace:index2', 'replace:appjs', 'replace:css', 'replace:css2', 'uglify:buildSynergy', 'cssmin', 'replace:replaceDatabaseName']);
+    grunt.registerTask('testBuild', ['default', 'uglify:buildTestSynergy', 'replace:testSynergyPartials', 'replace:testReplaceDatabaseName', 'replace:testSynergyResources']);
+};
diff --git a/synergy/README.md b/synergy/README.md
new file mode 100755
index 0000000..1700908
--- /dev/null
+++ b/synergy/README.md
@@ -0,0 +1,61 @@
+
+# How to install Synergy
+#### 1. Install Apache HTTP server, PHP and MYSQL
+Install Apache with PHP and MySQL:
+https://www.vultr.com/docs/how-to-install-apache-mysql-and-php-on-ubuntu-16-04
+
+Next enable .htaccess using steps from
+https://linode.com/docs/web-servers/apache/how-to-set-up-htaccess-on-apache/
+
+#### 2. Get Synergy sources
+assuming apache's document root is in `/var/www/html`, in terminal:
+
+```
+cd /var/www/html
+svn checkout https://svn.netbeans.org/svn/opensynergy~source-code-repository
+mv opensynergy~source-code-repository synergy
+cd synergy
+```
+Now you are in the synergy main directory.
+
+#### 3. Create database 
+this will also create default project, default version and a new user with username `import` and password `import` - this user is administrator, in terminal:
+
+```
+mysql -u root -p < server/db/structure.sql
+```
+
+#### 4. If needed change DB connection details
+update file `server/setup/conf.php` from line 18 which contains database connection details to match your database credentials
+
+```
+define('DHOST', 'mysql:host=localhost;dbname=synergy;charset=UTF8');
+define('DUSER', 'user');
+define('DPASS', 'password');
+define('DB', 'synergy');
+define('DBHOST', 'localhost');
+```
+
+#### 5. Now Synergy should be almost up and running
+you can try it in your browser `http://localhost/synergy/client/app/`
+
+#### 6. Setup directories for attachments and images
+By default, images will be stored at `/var/www/media/` and attachments at `/var/www/att/`. Either make sure these 2 paths exist and 
+that apache server can write there, or you'll need to change the setting to point to paths which meet the criteria.
+
+The change can be done via Synergy UI in `Administration -> Server` setting where you can find 2 setting fields: `ATTACHMENT_PATH` 
+and `IMAGE_PATH`. Note also the `IMAGE_BASE` which must correlate with `IMAGE_BASE`, for instance if `IMAGE_PATH` is `/var/www/html/images`, 
+then `IMAGE_BASE` would be `http://localhost/images/` . Please note that these 2 directories must have proper permissions - for local setup 777 is fine
+
+#### 7. Setting up error log file
+In terminal, navigate to the Synergy directory and open file `.htaccess`, line 33 starts with `php_value error_log` and 
+it ends with path to error log file. Synergy will print error logs there in case something is wrong. 
+
+Either create the default path and file (`/var/www/synergy/server/errors/errors.log`) or change the path in `.htaccess` file to your path. 
+Again make sure it is possible to write to file - setting 777 permissions should be on local deployment.
+
+#### 8. Allow Synergy to store static data from test runs
+In terminal in synergy directory, execute following:
+```
+chmod 777 -R server/data
+```
diff --git a/synergy/build.xml b/synergy/build.xml
new file mode 100755
index 0000000..d774c13
--- /dev/null
+++ b/synergy/build.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--to run:
+
+cd synergy
+ant -Dyui_compressor=/pathTo/yuicompressor-2.4.7.jar
+--> 
+<project name="Synergy build tool" default="build" basedir=".">
+    <property name="yui_compressor" value="/home/vriha/javalib/yuicompressor-2.4.7/build/yuicompressor-2.4.7.jar"/>
+    <property name="client_root" value="client/app"/>
+    <property name="min_css_folder" value="${basedir}/${client_root}/css/min"/>
+    <property name="css_folder" value="${basedir}/${client_root}/css"/>
+    <property name="min_js_folder" value="${basedir}/${client_root}/js/min"/>
+    <property name="js_folder" value="${basedir}/${client_root}/js"/>
+    <property name="index" value="${basedir}/${client_root}/index.html"/>
+    <property name="index_dev" value="${basedir}/${client_root}/index_dev.html"/>
+    <tstamp>
+        <format property="timestamp" pattern="mmss" />
+    </tstamp>
+
+    <target name="browser_cache">
+        <echo message="increasing cache parameters"/>
+        <exec executable="sed">
+            <arg line="-i s/synergy\.js?v=[0-9]*/synergy\.js?v=${timestamp}/ client/app/index.html"/>
+        </exec>
+        <exec executable="sed">
+            <arg line="-i s/synergy\.js?v=[0-9]*/synergy\.js?v=${timestamp}/ client/app/index2.html"/>
+        </exec>
+        <exec executable="sed">
+            <arg line="-i s/.html'/.html?v=${timestamp}'/ client/app/js/app.js"/>
+        </exec>
+    </target>
+    <target name="revert_browser_cache">
+        <echo message="removing partials cache parameters"/>
+         <exec executable="sed">
+            <arg line="-i s/.html?v=[0-9]*'/.html'/ client/app/js/app.js"/>
+        </exec>
+    </target>
+    
+    <target name="clean_css">
+        <delete>
+            <fileset dir="${min_css_folder}" includes="**/*.css"/>
+        </delete>
+        <echo message="Removing CSS files"/>
+    </target>
+    <target name="min_css" depends="clean_css">       
+        <echo message="minifying CSS files"/>
+        <apply executable="java"> 
+            <arg value="-jar"/> 
+            <arg value="${yui_compressor}"/> 
+            <arg line="--charset utf-8"/>
+            <arg line="--nomunge"/>
+            <srcfile/>
+            <arg line="-o"/>
+            <targetfile/>
+            <fileset dir="${css_folder}" includes="*.css"/> 
+            <mapper type="regexp" from="(.*)" to="${min_css_folder}/\0" />
+        </apply> 
+    </target>
+    <target name="clean_js">
+        <delete>
+            <fileset dir="${min_js_folder}" includes="**/*.js"/>
+        </delete>
+        <echo message="Removing JS files"/>
+    </target>
+    <target name="min_js" depends="clean_js">       
+        <echo message="minifying JS files"/>
+        <apply executable="java"> 
+            <arg value="-jar"/> 
+            <arg value="${yui_compressor}"/> 
+            <arg line="--charset utf-8"/>
+            <arg line="--nomunge"/>
+            <srcfile/>
+            <arg line="-o"/>
+            <targetfile/>
+            <fileset dir="${js_folder}" includes="*.js"/> 
+            <mapper type="regexp" from="(.*)" to="${min_js_folder}/\0" />
+        </apply> 
+    </target>
+    <target name="combine_js" depends="min_js">
+        <echo message="combining js files"/>
+        <concat destfile="${min_js_folder}/synergy.js">
+            <fileset dir="${min_js_folder}" includes="**/*.js"/>
+        </concat>
+    </target>
+    <target name="build" depends="min_css, browser_cache, min_js, combine_js, revert_browser_cache">
+    </target>
+</project>
\ No newline at end of file
diff --git a/synergy/client/app/admin.html b/synergy/client/app/admin.html
new file mode 100755
index 0000000..9f5d6ea
--- /dev/null
+++ b/synergy/client/app/admin.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<html lang="en" >
+    <head>
+        <meta charset="utf-8">
+        <title>Synergy - Administration</title>
+        <script src="js/bootstrap/jquery.js"></script>
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta name="description" content="">
+        <meta name="author" content="">
+        <link href="css/bootstrap.css" rel="stylesheet">
+        <link href="css/custom.css" rel="stylesheet">
+        <style type="text/css">
+            body {
+                padding-top: 60px;
+                padding-bottom: 40px;
+            }
+        </style>
+        <link href="css/bootstrap-responsive.css" rel="stylesheet">
+        <script src="js/libs/json/json2-min.js"></script>
+        <script src="js/configuration.js"></script>
+        <script src="js/utils.js"></script>
+        <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
+        <!--[if lt IE 9]>
+          <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
+        <![endif]-->
+
+
+    </head>
+
+    <body ng-app="synergy_admin">
+
+        <div class="navbar navbar-inverse navbar-fixed-top"  >
+
+
+
+            <div class="navbar-inner">
+                <div class="container">
+                    <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                    </a>
+                    <a class="brand" href="#">Synergy - administration</a>
+                    <div class="nav-collapse collapse" id="synergy_mainbar">
+                        <ul class="nav">
+                            <li class="active"><a href="index.html">Home</a></li>
+                            <li><a href="#/runs">Test Runs</a></li>
+                            <li><a href="#/versions">Versions</a></li>
+                            <li><a href="#/platforms">Platforms</a></li>
+                            <li><a href="#/users">Users</a></li>
+                            <li><a href="#/tribes">Tribes</a></li>
+                            
+                        </ul>
+                        <div ng-controller="SessionCtrl" id="synergy_session">
+                            <form class="navbar-form pull-right" id="synergy_login_form" >
+                                <input class="span2" type="text" placeholder="Email" ng-model="username" >
+                                <input class="span2" type="password" placeholder="Password"ng-model="password" >
+                                <button type="submit" class="btn" ng-click="login();">Sign in</button>
+                            </form>
+                            <div id="synergy_usermenu" style="display:none">
+                                <ul class="nav pull-right"><li class="dropdown"><a href="#" class="dropdown-toggle btn-primary" data-toggle="dropdown" id="usermenu_user" style="color: white">USER <b class="caret"></b></a>
+                                        <ul class="dropdown-menu"><li><a href="index.html#/user">Me</a></li><li><a href="#logout" ng-click="logout();">Logout</a></li>
+                                        </ul></li></ul>
+                            </div>
+                        </div>
+                    </div><!--/.nav-collapse -->
+                </div>
+            </div>
+        </div>
+
+        <div class="container-fluid">
+
+
+            <div class="row-fluid">
+            
+            </div>
+            <div class="row-fluid"  >
+
+
+
+                <div class="span12">
+
+
+
+                    <div ng-view></div>
+                </div>
+                <div class="span2"></div>
+            </div>
+            <!-- Main hero unit for a primary marketing message or call to action -->
+
+            <!-- Example row of columns -->
+
+
+            <hr>
+
+            <footer>
+                <p>&copy; Company 2012</p>
+            </footer>
+
+        </div> <!-- /container -->
+        <div class="modal fade hide" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+            <div class="modal-header">
+                <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+                <h3 id="myModalLabel">Modal header</h3>
+            </div>
+            <div class="modal-body" id="modal-body">
+                <p>One fine body 2…</p>
+            </div>
+            <div class="modal-footer">
+                <button class="btn btn-primary" data-dismiss="modal">OK</button>
+            </div>
+        </div>
+        <!-- Le javascript
+        ================================================== -->
+        <!-- Placed at the end of the document so the pages load faster -->
+
+        <script src="js/bootstrap/bootstrap-transition.js"></script>
+        <script src="js/bootstrap/bootstrap-alert.js"></script>
+        <script src="js/bootstrap/bootstrap-modal.js"></script>
+        <script src="js/bootstrap/bootstrap-dropdown.js"></script>
+        <script src="js/bootstrap/bootstrap-scrollspy.js"></script>
+        <script src="js/bootstrap/bootstrap-tab.js"></script>
+        <script src="js/bootstrap/bootstrap-tooltip.js"></script>
+        <script src="js/bootstrap/bootstrap-popover.js"></script>
+        <script src="js/bootstrap/bootstrap-button.js"></script>
+        <script src="js/bootstrap/bootstrap-collapse.js"></script>
+        <script src="js/bootstrap/bootstrap-carousel.js"></script>
+        <script src="js/bootstrap/bootstrap-typeahead.js"></script>
+        <script src="js/admin_controllers.js"></script>
+        <script src="lib/angular/angular.js"></script>
+        <script src="js/admin_app.js"></script>
+
+    </body>
+</html>
diff --git a/synergy/client/app/css/custom.css b/synergy/client/app/css/custom.css
new file mode 100755
index 0000000..0076569
--- /dev/null
+++ b/synergy/client/app/css/custom.css
@@ -0,0 +1,477 @@
+/* Custom keywords/labels for test cases */
+.label-fails, .badge-fails {
+    background-color: #333;
+}
+
+.label-sanity, .badge-sanity {
+    background-color: #B94A48;
+}
+
+.label-obsolete, .badge-obsolete {
+    background-color: #999;
+}
+
+
+/*custom labels end*/
+
+.dropbox{
+    border: 4px dashed rgba(0, 0, 0, 0.2);
+    text-align: center;
+    color: #999999;
+}
+.dropbox.hover{
+    border: 4px dashed #08C;
+}
+
+.table tbody tr.finished td, .table tbody tr td.finished, .finished, .row-passed td {
+    background-color: #dff0d8 !important;
+}
+
+.table tbody tr.unfinished td, .table tbody tr td.unfinished, .unfinished {
+    background-color: #fcf8e3 !important;
+}
+
+.table tbody tr.warning td, .table tbody tr td.warning, .warning, .row-failed td {
+    background-color: #fbdcbc !important;
+}
+
+.table tbody tr.pending td, .table tbody tr td.pending, .row-passed_with_issues td {
+    background-color: #d9edf7 !important;
+}
+
+.table-hover tbody tr.finished:hover td, .table tbody tr td.finished:hover, .row-passed:hover td{
+    background-color: #d0e9c6 !important;
+}
+
+.table-hover tbody tr.unfinished:hover td, .table tbody tr td.unfinished:hover {
+    background-color: #faf2cc !important;
+}
+
+.table-hover tbody tr.warning:hover td, .table tbody tr td.warning:hover, .row-failed:hover td {
+    background-color: #f8c592 !important;
+}
+
+.table-hover tbody tr.pending:hover td, .table tbody tr td.pending:hover, .row-passed_with_issues:hover td {
+    background-color: #c4e3f3 !important;
+}
+
+.ui-timepicker-div .ui-widget-header { margin-bottom: 8px; }
+.ui-timepicker-div dl { text-align: left; }
+.ui-timepicker-div dl dt { height: 25px; margin-bottom: -25px; }
+.ui-timepicker-div dl dd { margin: 0 10px 10px 65px; }
+.ui-timepicker-div td { font-size: 90%; }
+.ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; }
+
+.ui-timepicker-rtl{ direction: rtl; }
+.ui-timepicker-rtl dl { text-align: right; }
+.ui-timepicker-rtl dl dd { margin: 0 65px 10px 10px; }
+
+div.thumbnail > img:hover {
+    cursor: pointer;
+    -moz-border-radius: 4px;
+    border-radius: 4px;
+    border: 1px solid #0088cc;
+    -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25);
+    -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25);
+    box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25);
+}
+
+[ng\:cloak], [ng-cloak], .ng-cloak {
+    display: none !important;
+}
+
+.well, .well-large, .well-small, .table-bordered, .breadcrumb{
+    box-shadow: 2px 2px 2px #ccc;
+}
+#usermenu_user{
+    background-color: #006dcc !important;
+    background-image: none;
+    text-shadow: none;
+}
+
+.likelink{
+    color: #08c;
+    text-decoration: none;
+    cursor: pointer;
+}
+.likelink:hover{
+    color: #005580;
+    text-decoration: underline;
+    cursor: pointer;
+}
+/*.result pre{
+    background-color:white;
+    border:none;
+}*/
+
+.brand_busy{
+    color: #999;
+    -webkit-animation-name: glow;
+    -webkit-animation-duration: 1s;
+    -webkit-animation-iteration-count: infinite;
+    -ms-animation-name: glow;
+    -ms-animation-duration: 1s;
+    -ms-animation-iteration-count: infinite;
+    -moz-animation-name: glow;
+    -moz-animation-duration: 1s;
+    -moz-animation-iteration-count: infinite;
+    -o-animation-name: glow;
+    -o-animation-duration: 1s;
+    -o-animation-iteration-count: infinite;
+    animation-name: glow;
+    animation-duration: 1s;
+    animation-iteration-count: infinite;
+}
+@-webkit-keyframes glow{
+    0%{
+        color: #999;
+    }
+    10%{
+        color: #9197a4;
+    }
+    20%{
+        color: #7182aa;
+    }
+    30%{
+        color: #516db0;
+    }
+    40%{
+        color: #3057b6;
+    }
+    50%{
+        color: #1042bc;
+    }
+    60%{
+        color: #3057b6;
+    }
+    70%{
+        color: #516db0;
+    }
+    80%{
+        color: #7182aa;
+    }
+    90%{
+        color: #9197a4;
+    }
+    100%{
+        color: #999;
+    }
+}
+@-moz-keyframes glow{
+    0%{
+        color: #999;
+    }
+    10%{
+        color: #9197a4;
+    }
+    20%{
+        color: #7182aa;
+    }
+    30%{
+        color: #516db0;
+    }
+    40%{
+        color: #3057b6;
+    }
+    50%{
+        color: #1042bc;
+    }
+    60%{
+        color: #3057b6;
+    }
+    70%{
+        color: #516db0;
+    }
+    80%{
+        color: #7182aa;
+    }
+    90%{
+        color: #9197a4;
+    }
+    100%{
+        color: #999;
+    }
+}
+@keyframes glow{
+    0%{
+        color: #999;
+    }
+    10%{
+        color: #9197a4;
+    }
+    20%{
+        color: #7182aa;
+    }
+    30%{
+        color: #516db0;
+    }
+    40%{
+        color: #3057b6;
+    }
+    50%{
+        color: #1042bc;
+    }
+    60%{
+        color: #3057b6;
+    }
+    70%{
+        color: #516db0;
+    }
+    80%{
+        color: #7182aa;
+    }
+    90%{
+        color: #9197a4;
+    }
+    100%{
+        color: #999;
+    }
+}
+
+@-ms-keyframes glow{
+    0%{
+        color: #999;
+    }
+    10%{
+        color: #9197a4;
+    }
+    20%{
+        color: #7182aa;
+    }
+    30%{
+        color: #516db0;
+    }
+    40%{
+        color: #3057b6;
+    }
+    50%{
+        color: #1042bc;
+    }
+    60%{
+        color: #3057b6;
+    }
+    70%{
+        color: #516db0;
+    }
+    80%{
+        color: #7182aa;
+    }
+    90%{
+        color: #9197a4;
+    }
+    100%{
+        color: #999;
+    }
+}
+
+@-o-keyframes glow{
+    0%{
+        color: #999;
+    }
+    10%{
+        color: #9197a4;
+    }
+    20%{
+        color: #7182aa;
+    }
+    30%{
+        color: #516db0;
+    }
+    40%{
+        color: #3057b6;
+    }
+    50%{
+        color: #1042bc;
+    }
+    60%{
+        color: #3057b6;
+    }
+    70%{
+        color: #516db0;
+    }
+    80%{
+        color: #7182aa;
+    }
+    90%{
+        color: #9197a4;
+    }
+    100%{
+        color: #999;
+    }
+}
+
+.obsolete1{
+    color: gray;
+    font-style: italic;
+}
+.active0{
+    color: gray;
+    font-style: italic;
+}
+
+
+/* Styling for the ngProgress itself */
+#ngProgress {
+    margin: 0;
+    padding: 0;
+    z-index: 99998;
+    background-color: green;
+    color: green;
+    box-shadow: 0 0 10px 0;
+    /* Inherits the font color */
+
+    height: 2px;
+    opacity: 0;
+    /* Add CSS3 styles for transition smoothing */
+    -webkit-transition: all 0.5s ease-in-out;
+    -moz-transition: all 0.5s ease-in-out;
+    -o-transition: all 0.5s ease-in-out;
+    transition: all 0.5s ease-in-out;
+}
+
+/* Styling for the ngProgress-container */
+#ngProgress-container {
+    position: fixed;
+    margin: 0;
+    padding: 0;
+    top: 0;
+    left: 0;
+    right: 0;
+    z-index: 99999;
+}
+
+#userCaret{
+    border-top-color: #fff;
+    border-bottom-color: #fff;
+}
+body {
+    padding-top: 60px;
+    padding-bottom: 40px;
+}
+@media (max-width: 979px) {
+    body {
+        padding-top: 0px;
+    }
+}
+
+.bugs-error{
+    background-color: #f2dede;
+}
+.bugs-warning{
+    background-color: #fcf8e3;
+}
+.bugs-success{
+    background-color: #dff0d8;
+}
+.bugs-orange{
+    background-color: #ffcc66;
+}
+
+.userfalse, .specificationfalse, .platformfalse, .tribefalse, .passedfalse, .testedfalse, .timefalse, .testersfalse, .ratiofalse, .ttimefalse, .complfalse, .userfalse, .productivityfalse,
+.reviewerfalse,.reviewerRfalse,.complRfalse,.startedRfalse,.commentsRfalse, .weightRfalse,.timeRfalse {
+    opacity: 0.5;
+}
+.usertrue, .specificationtrue, .platformtrue, .tribetrue, .passedtrue, .testedtrue, .timetrue, .testerstrue, .ratiotrue, .ttimetrue, .compltrue, .usertrue, .productivitytrue,
+.reviewertrue,.reviewerRtrue,.complRtrue,.startedRtrue,.commentsRtrue, .weightRtrue,.timeRtrue{
+    opacity: 1;
+}
+.label-frozen{
+    background-color: #FF7878;
+}
+.small_cells td{
+    padding: 5px;
+}
+.casefailed{
+    background-color: #fbdcbc;
+}
+.casepassed{
+    background-color: #dff0d8;
+}
+.caseskipped{
+    background-color: #f5f5f5;
+}
+#cal .fc-header-title h2 {
+    font-size: .9em;
+    white-space: normal !important;
+}
+#cal .fc-view-month .fc-event, .fc-view-agendaWeek .fc-event {
+    font-size: 0;
+    overflow: hidden;
+    height: 2px;
+}
+#cal .fc-view-agendaWeek .fc-event-vert {
+    font-size: 0;
+    overflow: hidden;
+    width: 2px !important;
+}
+#cal .fc-agenda-axis {
+    width: 20px !important;
+    font-size: .7em;
+}
+
+#cal .fc-button-content {
+    padding: 0;
+}
+#typeahead_search{
+    display: block;
+    visibility: visible
+}
+.owner_formerUser{
+    color: #888;
+}
+
+.clockIcon{
+    width: 1.5em;
+}
+.label-aborted, .badge-aborted,.label-ABORTED, .badge-ABORTED, .label-disabled, .badge-disabled,.label-DISABLED, .badge-DISABLED {
+    background-color: #999;   
+}
+.label-failed, .badge-failed,.label-FAILED, .badge-FAILED, .label-failure, .badge-failure,.label-FAILURE, .badge-FAILURE {
+    background-color: #B94A48;
+}
+.label-unstable, .badge-unstable,.label-UNSTABLE, .badge-UNSTABLE {
+    background-color: #f89406;
+}
+.label-SUCCESS, .badge-SUCCESS{
+    background-color: #468847;
+}
+.jobContainer{
+    margin: 0.5em 0;
+}
+.label-big{
+    font-size: 1.3em;
+    margin: 0.5em;
+}
+.grey{
+    color: #777;
+}
+.specVisiblefalse, .platformVisiblefalse{
+    color: #aaa;
+}
+.bold{
+    font-weight: bold;
+}
+.x-controls{
+    font-size: 14px;
+    margin: 1em 0 2em 0;
+}
+.x-controls label, .x-controls input{
+    display: inline !important;
+    margin: 0 !important;
+}
+.x-controls .pull-left{
+    margin: 0 1em 0 0;
+}
+.x-controls .pull-left:nth-child(2){
+    margin: 0 1em 0 2em;
+}
+.x-controls .select2-search-choice-close:after {
+    content: 'x';
+    width: 14px;
+    height: 14px;
+    color: #000;
+    font-size: 14px;
+}
+.x-labels{
+    width: 30vw;
+}
\ No newline at end of file
diff --git a/synergy/client/app/css/docs.css b/synergy/client/app/css/docs.css
new file mode 100755
index 0000000..cd592f9
--- /dev/null
+++ b/synergy/client/app/css/docs.css
@@ -0,0 +1,1001 @@
+/* Add additional stylesheets below
+-------------------------------------------------- */
+/*
+  Bootstrap's documentation styles
+  Special styles for presenting Bootstrap's documentation and examples
+*/
+
+
+
+/* Body and structure
+-------------------------------------------------- */
+
+body {
+  position: relative;
+  padding-top: 40px;
+}
+
+/* Code in headings */
+h3 code {
+  font-size: 14px;
+  font-weight: normal;
+}
+
+
+
+/* Tweak navbar brand link to be super sleek
+-------------------------------------------------- */
+
+body > .navbar {
+  font-size: 13px;
+}
+
+/* Change the docs' brand */
+body > .navbar .brand {
+  padding-right: 0;
+  padding-left: 0;
+  margin-left: 20px;
+  float: right;
+  font-weight: bold;
+  color: #000;
+  text-shadow: 0 1px 0 rgba(255,255,255,.1), 0 0 30px rgba(255,255,255,.125);
+  -webkit-transition: all .2s linear;
+     -moz-transition: all .2s linear;
+          transition: all .2s linear;
+}
+body > .navbar .brand:hover {
+  text-decoration: none;
+  text-shadow: 0 1px 0 rgba(255,255,255,.1), 0 0 30px rgba(255,255,255,.4);
+}
+
+
+/* Sections
+-------------------------------------------------- */
+
+/* padding for in-page bookmarks and fixed navbar */
+section {
+  padding-top: 30px;
+}
+section > .page-header,
+section > .lead {
+  color: #5a5a5a;
+}
+section > ul li {
+  margin-bottom: 5px;
+}
+
+/* Separators (hr) */
+.bs-docs-separator {
+  margin: 40px 0 39px;
+}
+
+/* Faded out hr */
+hr.soften {
+  height: 1px;
+  margin: 70px 0;
+  background-image: -webkit-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0));
+  background-image:    -moz-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0));
+  background-image:     -ms-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0));
+  background-image:      -o-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0));
+  border: 0;
+}
+
+
+
+/* Jumbotrons
+-------------------------------------------------- */
+
+/* Base class
+------------------------- */
+.jumbotron {
+  position: relative;
+  padding: 40px 0;
+  color: #fff;
+  text-align: center;
+  text-shadow: 0 1px 3px rgba(0,0,0,.4), 0 0 30px rgba(0,0,0,.075);
+  background: #020031; /* Old browsers */
+  background: -moz-linear-gradient(45deg,  #020031 0%, #6d3353 100%); /* FF3.6+ */
+  background: -webkit-gradient(linear, left bottom, right top, color-stop(0%,#020031), color-stop(100%,#6d3353)); /* Chrome,Safari4+ */
+  background: -webkit-linear-gradient(45deg,  #020031 0%,#6d3353 100%); /* Chrome10+,Safari5.1+ */
+  background: -o-linear-gradient(45deg,  #020031 0%,#6d3353 100%); /* Opera 11.10+ */
+  background: -ms-linear-gradient(45deg,  #020031 0%,#6d3353 100%); /* IE10+ */
+  background: linear-gradient(45deg,  #020031 0%,#6d3353 100%); /* W3C */
+  filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#020031', endColorstr='#6d3353',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */
+  -webkit-box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2);
+     -moz-box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2);
+          box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2);
+}
+.jumbotron h1 {
+  font-size: 80px;
+  font-weight: bold;
+  letter-spacing: -1px;
+  line-height: 1;
+}
+.jumbotron p {
+  font-size: 24px;
+  font-weight: 300;
+  line-height: 30px;
+  margin-bottom: 30px;
+}
+
+/* Link styles (used on .masthead-links as well) */
+.jumbotron a {
+  color: #fff;
+  color: rgba(255,255,255,.5);
+  -webkit-transition: all .2s ease-in-out;
+     -moz-transition: all .2s ease-in-out;
+          transition: all .2s ease-in-out;
+}
+.jumbotron a:hover {
+  color: #fff;
+  text-shadow: 0 0 10px rgba(255,255,255,.25);
+}
+
+/* Download button */
+.masthead .btn {
+  padding: 14px 24px;
+  font-size: 24px;
+  font-weight: 200;
+  color: #fff; /* redeclare to override the `.jumbotron a` */
+  border: 0;
+  -webkit-border-radius: 6px;
+     -moz-border-radius: 6px;
+          border-radius: 6px;
+  -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25);
+     -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25);
+          box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25);
+  -webkit-transition: none;
+     -moz-transition: none;
+          transition: none;
+}
+.masthead .btn:hover {
+  -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25);
+     -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25);
+          box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25);
+}
+.masthead .btn:active {
+  -webkit-box-shadow: inset 0 2px 4px rgba(0,0,0,.1), 0 1px 0 rgba(255,255,255,.1);
+     -moz-box-shadow: inset 0 2px 4px rgba(0,0,0,.1), 0 1px 0 rgba(255,255,255,.1);
+          box-shadow: inset 0 2px 4px rgba(0,0,0,.1), 0 1px 0 rgba(255,255,255,.1);
+}
+
+
+/* Pattern overlay
+------------------------- */
+.jumbotron .container {
+  position: relative;
+  z-index: 2;
+}
+.jumbotron:after {
+  content: '';
+  display: block;
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  background: url(../img/bs-docs-masthead-pattern.png) repeat center center;
+  opacity: .4;
+}
+
+/* Masthead (docs home)
+------------------------- */
+.masthead {
+  padding: 70px 0 80px;
+  margin-bottom: 0;
+  color: #fff;
+}
+.masthead h1 {
+  font-size: 120px;
+  line-height: 1;
+  letter-spacing: -2px;
+}
+.masthead p {
+  font-size: 40px;
+  font-weight: 200;
+  line-height: 1.25;
+}
+
+/* Textual links in masthead */
+.masthead-links {
+  margin: 0;
+  list-style: none;
+}
+.masthead-links li {
+  display: inline;
+  padding: 0 10px;
+  color: rgba(255,255,255,.25);
+}
+
+/* Social proof buttons from GitHub & Twitter */
+.bs-docs-social {
+  padding: 15px 0;
+  text-align: center;
+  background-color: #f5f5f5;
+  border-top: 1px solid #fff;
+  border-bottom: 1px solid #ddd;
+}
+
+/* Quick links on Home */
+.bs-docs-social-buttons {
+  margin-left: 0;
+  margin-bottom: 0;
+  padding-left: 0;
+  list-style: none;
+}
+.bs-docs-social-buttons li {
+  display: inline-block;
+  padding: 5px 8px;
+  line-height: 1;
+  *display: inline;
+  *zoom: 1;
+}
+
+/* Subhead (other pages)
+------------------------- */
+.subhead {
+  text-align: left;
+  border-bottom: 1px solid #ddd;
+}
+.subhead h1 {
+  font-size: 60px;
+}
+.subhead p {
+  margin-bottom: 20px;
+}
+.subhead .navbar {
+  display: none;
+}
+
+
+
+/* Marketing section of Overview
+-------------------------------------------------- */
+
+.marketing {
+  text-align: center;
+  color: #5a5a5a;
+}
+.marketing h1 {
+  margin: 60px 0 10px;
+  font-size: 60px;
+  font-weight: 200;
+  line-height: 1;
+  letter-spacing: -1px;
+}
+.marketing h2 {
+  font-weight: 200;
+  margin-bottom: 5px;
+}
+.marketing p {
+  font-size: 16px;
+  line-height: 1.5;
+}
+.marketing .marketing-byline {
+  margin-bottom: 40px;
+  font-size: 20px;
+  font-weight: 300;
+  line-height: 25px;
+  color: #999;
+}
+.marketing img {
+  display: block;
+  margin: 0 auto 30px;
+}
+
+
+
+/* Footer
+-------------------------------------------------- */
+
+.footer {
+  padding: 70px 0;
+  margin-top: 70px;
+  border-top: 1px solid #e5e5e5;
+  background-color: #f5f5f5;
+}
+.footer p {
+  margin-bottom: 0;
+  color: #777;
+}
+.footer-links {
+  margin: 10px 0;
+}
+.footer-links li {
+  display: inline;
+  margin-right: 10px;
+}
+
+
+
+/* Special grid styles
+-------------------------------------------------- */
+
+.show-grid {
+  margin-top: 10px;
+  margin-bottom: 20px;
+}
+.show-grid [class*="span"] {
+  background-color: #eee;
+  text-align: center;
+  -webkit-border-radius: 3px;
+     -moz-border-radius: 3px;
+          border-radius: 3px;
+  min-height: 40px;
+  line-height: 40px;
+}
+.show-grid:hover [class*="span"] {
+  background: #ddd;
+}
+.show-grid .show-grid {
+  margin-top: 0;
+  margin-bottom: 0;
+}
+.show-grid .show-grid [class*="span"] {
+  background-color: #ccc;
+}
+
+
+
+/* Mini layout previews
+-------------------------------------------------- */
+.mini-layout {
+  border: 1px solid #ddd;
+  -webkit-border-radius: 6px;
+     -moz-border-radius: 6px;
+          border-radius: 6px;
+  -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.075);
+     -moz-box-shadow: 0 1px 2px rgba(0,0,0,.075);
+          box-shadow: 0 1px 2px rgba(0,0,0,.075);
+}
+.mini-layout,
+.mini-layout .mini-layout-body,
+.mini-layout.fluid .mini-layout-sidebar {
+  height: 300px;
+}
+.mini-layout {
+  margin-bottom: 20px;
+  padding: 9px;
+}
+.mini-layout div {
+  -webkit-border-radius: 3px;
+     -moz-border-radius: 3px;
+          border-radius: 3px;
+}
+.mini-layout .mini-layout-body {
+  background-color: #dceaf4;
+  margin: 0 auto;
+  width: 70%;
+}
+.mini-layout.fluid .mini-layout-sidebar,
+.mini-layout.fluid .mini-layout-header,
+.mini-layout.fluid .mini-layout-body {
+  float: left;
+}
+.mini-layout.fluid .mini-layout-sidebar {
+  background-color: #bbd8e9;
+  width: 20%;
+}
+.mini-layout.fluid .mini-layout-body {
+  width: 77.5%;
+  margin-left: 2.5%;
+}
+
+
+
+/* Download page
+-------------------------------------------------- */
+
+.download .page-header {
+  margin-top: 36px;
+}
+.page-header .toggle-all {
+  margin-top: 5px;
+}
+
+/* Space out h3s when following a section */
+.download h3 {
+  margin-bottom: 5px;
+}
+.download-builder input + h3,
+.download-builder .checkbox + h3 {
+  margin-top: 9px;
+}
+
+/* Fields for variables */
+.download-builder input[type=text] {
+  margin-bottom: 9px;
+  font-family: Menlo, Monaco, "Courier New", monospace;
+  font-size: 12px;
+  color: #d14;
+}
+.download-builder input[type=text]:focus {
+  background-color: #fff;
+}
+
+/* Custom, larger checkbox labels */
+.download .checkbox {
+  padding: 6px 10px 6px 25px;
+  font-size: 13px;
+  line-height: 18px;
+  color: #555;
+  background-color: #f9f9f9;
+  -webkit-border-radius: 3px;
+     -moz-border-radius: 3px;
+          border-radius: 3px;
+  cursor: pointer;
+}
+.download .checkbox:hover {
+  color: #333;
+  background-color: #f5f5f5;
+}
+.download .checkbox small {
+  font-size: 12px;
+  color: #777;
+}
+
+/* Variables section */
+#variables label {
+  margin-bottom: 0;
+}
+
+/* Giant download button */
+.download-btn {
+  margin: 36px 0 108px;
+}
+#download p,
+#download h4 {
+  max-width: 50%;
+  margin: 0 auto;
+  color: #999;
+  text-align: center;
+}
+#download h4 {
+  margin-bottom: 0;
+}
+#download p {
+  margin-bottom: 18px;
+}
+.download-btn .btn {
+  display: block;
+  width: auto;
+  padding: 19px 24px;
+  margin-bottom: 27px;
+  font-size: 30px;
+  line-height: 1;
+  text-align: center;
+  -webkit-border-radius: 6px;
+     -moz-border-radius: 6px;
+          border-radius: 6px;
+}
+
+
+
+/* Misc
+-------------------------------------------------- */
+
+/* Make tables spaced out a bit more */
+h2 + table,
+h3 + table,
+h4 + table,
+h2 + .row {
+  margin-top: 5px;
+}
+
+/* Example sites showcase */
+.example-sites {
+  xmargin-left: 20px;
+}
+.example-sites img {
+  max-width: 100%;
+  margin: 0 auto;
+}
+
+.scrollspy-example {
+  height: 200px;
+  overflow: auto;
+  position: relative;
+}
+
+
+/* Fake the :focus state to demo it */
+.focused {
+  border-color: rgba(82,168,236,.8);
+  -webkit-box-shadow: inset 0 1px 3px rgba(0,0,0,.1), 0 0 8px rgba(82,168,236,.6);
+     -moz-box-shadow: inset 0 1px 3px rgba(0,0,0,.1), 0 0 8px rgba(82,168,236,.6);
+          box-shadow: inset 0 1px 3px rgba(0,0,0,.1), 0 0 8px rgba(82,168,236,.6);
+  outline: 0;
+}
+
+/* For input sizes, make them display block */
+.docs-input-sizes select,
+.docs-input-sizes input[type=text] {
+  display: block;
+  margin-bottom: 9px;
+}
+
+/* Icons
+------------------------- */
+.the-icons {
+  margin-left: 0;
+  list-style: none;
+}
+.the-icons li {
+  float: left;
+  width: 25%;
+  line-height: 25px;
+}
+.the-icons i:hover {
+  background-color: rgba(255,0,0,.25);
+}
+
+/* Example page
+------------------------- */
+.bootstrap-examples p {
+  font-size: 13px;
+  line-height: 18px;
+}
+.bootstrap-examples .thumbnail {
+  margin-bottom: 9px;
+  background-color: #fff;
+}
+
+
+
+/* Bootstrap code examples
+-------------------------------------------------- */
+
+/* Base class */
+.bs-docs-example {
+  position: relative;
+  margin: 15px 0;
+  padding: 39px 19px 14px;
+  *padding-top: 19px;
+  background-color: #fff;
+  border: 1px solid #ddd;
+  -webkit-border-radius: 4px;
+     -moz-border-radius: 4px;
+          border-radius: 4px;
+}
+
+/* Echo out a label for the example */
+.bs-docs-example:after {
+  content: "Example";
+  position: absolute;
+  top: -1px;
+  left: -1px;
+  padding: 3px 7px;
+  font-size: 12px;
+  font-weight: bold;
+  background-color: #f5f5f5;
+  border: 1px solid #ddd;
+  color: #9da0a4;
+  -webkit-border-radius: 4px 0 4px 0;
+     -moz-border-radius: 4px 0 4px 0;
+          border-radius: 4px 0 4px 0;
+}
+
+/* Remove spacing between an example and it's code */
+.bs-docs-example + .prettyprint {
+  margin-top: -20px;
+  padding-top: 15px;
+}
+
+/* Tweak examples
+------------------------- */
+.bs-docs-example > p:last-child {
+  margin-bottom: 0;
+}
+.bs-docs-example .table,
+.bs-docs-example .progress,
+.bs-docs-example .well,
+.bs-docs-example .alert,
+.bs-docs-example .hero-unit,
+.bs-docs-example .pagination,
+.bs-docs-example .navbar,
+.bs-docs-example > .nav,
+.bs-docs-example blockquote {
+  margin-bottom: 5px;
+}
+.bs-docs-example .pagination {
+  margin-top: 0;
+}
+.bs-navbar-top-example,
+.bs-navbar-bottom-example {
+  z-index: 1;
+  padding: 0;
+  height: 90px;
+  overflow: hidden; /* cut the drop shadows off */
+}
+.bs-navbar-top-example .navbar-fixed-top,
+.bs-navbar-bottom-example .navbar-fixed-bottom {
+  margin-left: 0;
+  margin-right: 0;
+}
+.bs-navbar-top-example {
+  -webkit-border-radius: 0 0 4px 4px;
+     -moz-border-radius: 0 0 4px 4px;
+          border-radius: 0 0 4px 4px;
+}
+.bs-navbar-top-example:after {
+  top: auto;
+  bottom: -1px;
+  -webkit-border-radius: 0 4px 0 4px;
+     -moz-border-radius: 0 4px 0 4px;
+          border-radius: 0 4px 0 4px;
+}
+.bs-navbar-bottom-example {
+  -webkit-border-radius: 4px 4px 0 0;
+     -moz-border-radius: 4px 4px 0 0;
+          border-radius: 4px 4px 0 0;
+}
+.bs-navbar-bottom-example .navbar {
+  margin-bottom: 0;
+}
+form.bs-docs-example {
+  padding-bottom: 19px;
+}
+
+/* Images */
+.bs-docs-example-images img {
+  margin: 10px;
+  display: inline-block;
+}
+
+/* Tooltips */
+.bs-docs-tooltip-examples {
+  text-align: center;
+  margin: 0 0 10px;
+  list-style: none;
+}
+.bs-docs-tooltip-examples li {
+  display: inline;
+  padding: 0 10px;
+}
+
+/* Popovers */
+.bs-docs-example-popover {
+  padding-bottom: 24px;
+  background-color: #f9f9f9;
+}
+.bs-docs-example-popover .popover {
+  position: relative;
+  display: block;
+  float: left;
+  width: 260px;
+  margin: 20px;
+}
+
+
+
+/* Responsive docs
+-------------------------------------------------- */
+
+/* Utility classes table
+------------------------- */
+.responsive-utilities th small {
+  display: block;
+  font-weight: normal;
+  color: #999;
+}
+.responsive-utilities tbody th {
+  font-weight: normal;
+}
+.responsive-utilities td {
+  text-align: center;
+}
+.responsive-utilities td.is-visible {
+  color: #468847;
+  background-color: #dff0d8 !important;
+}
+.responsive-utilities td.is-hidden {
+  color: #ccc;
+  background-color: #f9f9f9 !important;
+}
+
+/* Responsive tests
+------------------------- */
+.responsive-utilities-test {
+  margin-top: 5px;
+  margin-left: 0;
+  list-style: none;
+  overflow: hidden; /* clear floats */
+}
+.responsive-utilities-test li {
+  position: relative;
+  float: left;
+  width: 25%;
+  height: 43px;
+  font-size: 14px;
+  font-weight: bold;
+  line-height: 43px;
+  color: #999;
+  text-align: center;
+  border: 1px solid #ddd;
+  -webkit-border-radius: 4px;
+     -moz-border-radius: 4px;
+          border-radius: 4px;
+}
+.responsive-utilities-test li + li {
+  margin-left: 10px;
+}
+.responsive-utilities-test span {
+  position: absolute;
+  top:    -1px;
+  left:   -1px;
+  right:  -1px;
+  bottom: -1px;
+  -webkit-border-radius: 4px;
+     -moz-border-radius: 4px;
+          border-radius: 4px;
+}
+.responsive-utilities-test span {
+  color: #468847;
+  background-color: #dff0d8;
+  border: 1px solid #d6e9c6;
+}
+
+
+
+/* Sidenav for Docs
+-------------------------------------------------- */
+
+.bs-docs-sidenav {
+  width: 228px;
+  margin: 30px 0 0;
+  padding: 0;
+  background-color: #fff;
+  -webkit-border-radius: 6px;
+     -moz-border-radius: 6px;
+          border-radius: 6px;
+  -webkit-box-shadow: 0 1px 4px rgba(0,0,0,.065);
+     -moz-box-shadow: 0 1px 4px rgba(0,0,0,.065);
+          box-shadow: 0 1px 4px rgba(0,0,0,.065);
+}
+.bs-docs-sidenav > li > a {
+  display: block;
+  *width: 190px;
+  margin: 0 0 -1px;
+  padding: 8px 14px;
+  border: 1px solid #e5e5e5;
+}
+.bs-docs-sidenav > li:first-child > a {
+  -webkit-border-radius: 6px 6px 0 0;
+     -moz-border-radius: 6px 6px 0 0;
+          border-radius: 6px 6px 0 0;
+}
+.bs-docs-sidenav > li:last-child > a {
+  -webkit-border-radius: 0 0 6px 6px;
+     -moz-border-radius: 0 0 6px 6px;
+          border-radius: 0 0 6px 6px;
+}
+.bs-docs-sidenav > .active > a {
+  position: relative;
+  z-index: 2;
+  padding: 9px 15px;
+  border: 0;
+  text-shadow: 0 1px 0 rgba(0,0,0,.15);
+  -webkit-box-shadow: inset 1px 0 0 rgba(0,0,0,.1), inset -1px 0 0 rgba(0,0,0,.1);
+     -moz-box-shadow: inset 1px 0 0 rgba(0,0,0,.1), inset -1px 0 0 rgba(0,0,0,.1);
+          box-shadow: inset 1px 0 0 rgba(0,0,0,.1), inset -1px 0 0 rgba(0,0,0,.1);
+}
+/* Chevrons */
+.bs-docs-sidenav .icon-chevron-right {
+  float: right;
+  margin-top: 2px;
+  margin-right: -6px;
+  opacity: .25;
+}
+.bs-docs-sidenav > li > a:hover {
+  background-color: #f5f5f5;
+}
+.bs-docs-sidenav a:hover .icon-chevron-right {
+  opacity: .5;
+}
+.bs-docs-sidenav .active .icon-chevron-right,
+.bs-docs-sidenav .active a:hover .icon-chevron-right {
+  background-image: url(../../img/glyphicons-halflings-white.png);
+  opacity: 1;
+}
+.bs-docs-sidenav.affix {
+  top: 40px;
+}
+.bs-docs-sidenav.affix-bottom {
+  position: absolute;
+  top: auto;
+  bottom: 270px;
+}
+
+
+
+
+/* Responsive
+-------------------------------------------------- */
+
+/* Desktop large
+------------------------- */
+@media (min-width: 1200px) {
+  .bs-docs-container {
+    max-width: 970px;
+  }
+  .bs-docs-sidenav {
+    width: 258px;
+  }
+}
+
+/* Desktop
+------------------------- */
+@media (max-width: 980px) {
+  /* Unfloat brand */
+  body > .navbar-fixed-top .brand {
+    float: left;
+    margin-left: 0;
+    padding-left: 10px;
+    padding-right: 10px;
+  }
+
+  /* Inline-block quick links for more spacing */
+  .quick-links li {
+    display: inline-block;
+    margin: 5px;
+  }
+
+  /* When affixed, space properly */
+  .bs-docs-sidenav {
+    top: 0;
+    margin-top: 30px;
+    margin-right: 0;
+  }
+}
+
+/* Tablet to desktop
+------------------------- */
+@media (min-width: 768px) and (max-width: 980px) {
+  /* Remove any padding from the body */
+  body {
+    padding-top: 0;
+  }
+  /* Widen masthead and social buttons to fill body padding */
+  .jumbotron {
+    margin-top: -20px; /* Offset bottom margin on .navbar */
+  }
+  /* Adjust sidenav width */
+  .bs-docs-sidenav {
+    width: 166px;
+    margin-top: 20px;
+  }
+  .bs-docs-sidenav.affix {
+    top: 0;
+  }
+}
+
+/* Tablet
+------------------------- */
+@media (max-width: 767px) {
+  /* Remove any padding from the body */
+  body {
+    padding-top: 0;
+  }
+
+  /* Widen masthead and social buttons to fill body padding */
+  .jumbotron {
+    padding: 40px 20px;
+    margin-top:   -20px; /* Offset bottom margin on .navbar */
+    margin-right: -20px;
+    margin-left:  -20px;
+  }
+  .masthead h1 {
+    font-size: 90px;
+  }
+  .masthead p,
+  .masthead .btn {
+    font-size: 24px;
+  }
+  .marketing .span4 {
+    margin-bottom: 40px;
+  }
+  .bs-docs-social {
+    margin: 0 -20px;
+  }
+
+  /* Space out the show-grid examples */
+  .show-grid [class*="span"] {
+    margin-bottom: 5px;
+  }
+
+  /* Sidenav */
+  .bs-docs-sidenav {
+    width: auto;
+    margin-bottom: 20px;
+  }
+  .bs-docs-sidenav.affix {
+    position: static;
+    width: auto;
+    top: 0;
+  }
+
+  /* Unfloat the back to top link in footer */
+  .footer {
+    margin-left: -20px;
+    margin-right: -20px;
+    padding-left: 20px;
+    padding-right: 20px;
+  }
+  .footer p {
+    margin-bottom: 9px;
+  }
+}
+
+/* Landscape phones
+------------------------- */
+@media (max-width: 480px) {
+  /* Remove padding above jumbotron */
+  body {
+    padding-top: 0;
+  }
+
+  /* Change up some type stuff */
+  h2 small {
+    display: block;
+  }
+
+  /* Downsize the jumbotrons */
+  .jumbotron h1 {
+    font-size: 60px;
+  }
+  .jumbotron p,
+  .jumbotron .btn {
+    font-size: 20px;
+  }
+  .jumbotron .btn {
+    display: block;
+    margin: 0 auto;
+  }
+
+  /* center align subhead text like the masthead */
+  .subhead h1,
+  .subhead p {
+    text-align: center;
+  }
+
+  /* Marketing on home */
+  .marketing h1 {
+    font-size: 40px;
+  }
+
+  /* center example sites */
+  .example-sites {
+    margin-left: 0;
+  }
+  .example-sites > li {
+    float: none;
+    display: block;
+    max-width: 280px;
+    margin: 0 auto 18px;
+    text-align: center;
+  }
+  .example-sites .thumbnail > img {
+    max-width: 270px;
+  }
+
+  /* Do our best to make tables work in narrow viewports */
+  table code {
+    white-space: normal;
+    word-wrap: break-word;
+    word-break: break-all;
+  }
+
+  /* Modal example */
+  .modal-example .modal {
+    position: relative;
+    top: auto;
+    right: auto;
+    bottom: auto;
+    left: auto;
+  }
+
+  /* Unfloat the back to top in footer to prevent odd text wrapping */
+  .footer .pull-right {
+    float: none;
+  }
+}
diff --git a/synergy/client/app/css/min/custom.css b/synergy/client/app/css/min/custom.css
new file mode 100644
index 0000000..7eab304
--- /dev/null
+++ b/synergy/client/app/css/min/custom.css
@@ -0,0 +1 @@
+.badge-fails,.label-fails{background-color:#333}.badge-sanity,.label-sanity{background-color:#b94a48}.badge-obsolete,.label-obsolete{background-color:#999}.dropbox{border:4px dashed rgba(0,0,0,.2);text-align:center;color:#999}.dropbox.hover{border:4px dashed #08c}.finished,.row-passed td,.table tbody tr td.finished,.table tbody tr.finished td{background-color:#dff0d8!important}.table tbody tr td.unfinished,.table tbody tr.unfinished td,.unfinished{background-color:#fcf8e3!important}.row- [...]
\ No newline at end of file
diff --git a/synergy/client/app/css/min/docs.css b/synergy/client/app/css/min/docs.css
new file mode 100755
index 0000000..5360e03
--- /dev/null
+++ b/synergy/client/app/css/min/docs.css
@@ -0,0 +1 @@
+body{position:relative;padding-top:40px}h3 code{font-size:14px;font-weight:400}body>.navbar{font-size:13px}body>.navbar .brand{padding-right:0;padding-left:0;margin-left:20px;float:right;font-weight:700;color:#000;text-shadow:0 1px 0 rgba(255,255,255,.1),0 0 30px rgba(255,255,255,.125);-webkit-transition:all .2s linear;-moz-transition:all .2s linear;transition:all .2s linear}body>.navbar .brand:hover{text-decoration:none;text-shadow:0 1px 0 rgba(255,255,255,.1),0 0 30px rgba(255,255,255, [...]
\ No newline at end of file
diff --git a/synergy/client/app/favicon.ico b/synergy/client/app/favicon.ico
new file mode 100755
index 0000000..902c0f2
Binary files /dev/null and b/synergy/client/app/favicon.ico differ
diff --git a/synergy/client/app/img/ajax-loader.gif b/synergy/client/app/img/ajax-loader.gif
new file mode 100644
index 0000000..e377924
Binary files /dev/null and b/synergy/client/app/img/ajax-loader.gif differ
diff --git a/synergy/client/app/img/blue.png b/synergy/client/app/img/blue.png
new file mode 100644
index 0000000..17efcae
Binary files /dev/null and b/synergy/client/app/img/blue.png differ
diff --git a/synergy/client/app/img/bs-docs-bootstrap-features.png b/synergy/client/app/img/bs-docs-bootstrap-features.png
new file mode 100644
index 0000000..bbdbee4
Binary files /dev/null and b/synergy/client/app/img/bs-docs-bootstrap-features.png differ
diff --git a/synergy/client/app/img/bs-docs-masthead-pattern.png b/synergy/client/app/img/bs-docs-masthead-pattern.png
new file mode 100644
index 0000000..a1afdad
Binary files /dev/null and b/synergy/client/app/img/bs-docs-masthead-pattern.png differ
diff --git a/synergy/client/app/img/clock.png b/synergy/client/app/img/clock.png
new file mode 100644
index 0000000..22b1f6a
Binary files /dev/null and b/synergy/client/app/img/clock.png differ
diff --git a/synergy/client/app/img/glyphicons-halflings-white.png b/synergy/client/app/img/glyphicons-halflings-white.png
new file mode 100644
index 0000000..da199eb
Binary files /dev/null and b/synergy/client/app/img/glyphicons-halflings-white.png differ
diff --git a/synergy/client/app/img/glyphicons-halflings.png b/synergy/client/app/img/glyphicons-halflings.png
new file mode 100644
index 0000000..05373d1
Binary files /dev/null and b/synergy/client/app/img/glyphicons-halflings.png differ
diff --git a/synergy/client/app/img/grey.png b/synergy/client/app/img/grey.png
new file mode 100644
index 0000000..89957ae
Binary files /dev/null and b/synergy/client/app/img/grey.png differ
diff --git a/synergy/client/app/img/grid-baseline-20px.png b/synergy/client/app/img/grid-baseline-20px.png
new file mode 100644
index 0000000..cf6f98c
Binary files /dev/null and b/synergy/client/app/img/grid-baseline-20px.png differ
diff --git a/synergy/client/app/img/nb.gif b/synergy/client/app/img/nb.gif
new file mode 100644
index 0000000..c5a63d9
Binary files /dev/null and b/synergy/client/app/img/nb.gif differ
diff --git a/synergy/client/app/img/red.png b/synergy/client/app/img/red.png
new file mode 100644
index 0000000..0bcd944
Binary files /dev/null and b/synergy/client/app/img/red.png differ
diff --git a/synergy/client/app/img/user.png b/synergy/client/app/img/user.png
new file mode 100644
index 0000000..9fdd05d
Binary files /dev/null and b/synergy/client/app/img/user.png differ
diff --git a/synergy/client/app/img/yellow.png b/synergy/client/app/img/yellow.png
new file mode 100644
index 0000000..f887f2d
Binary files /dev/null and b/synergy/client/app/img/yellow.png differ
diff --git a/synergy/client/app/index.html b/synergy/client/app/index.html
new file mode 100755
index 0000000..d739b36
--- /dev/null
+++ b/synergy/client/app/index.html
@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<html lang="en" xmlns:ng="http://angularjs.org" id="ng-app" data-ng-app="synergy" class="ng-app:synergy">
+    <head>
+        <link rel="SHORTCUT ICON" href="favicon.ico"/>
+        <meta charset="utf-8">
+        <link href="./opensearch.xml" rel="search" title="synergy search" type="application/opensearchdescription+xml">
+        <meta http-equiv="X-UA-Compatible" content="IE=10" />
+        <title>Synergy - Test Management Tool</title>
+        <link rel="stylesheet" href="lib/angular-ui/fullcalendar.min.css" >
+        <link rel="stylesheet" type="text/css" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.22/themes/base/jquery-ui.css">
+        <link href="css/min/bootstrap.css?v=1" rel="stylesheet">
+        <link href="css/min/custom.css?v=1521293430023" rel="stylesheet">
+        <link rel="stylesheet" type="text/css" href="js/libs/codemirror/codemirror.min.css">
+        <link href="css/min/bootstrap-responsive.css" rel="stylesheet">
+        <link href="lib/angular-ui/select2.min.css" rel="stylesheet">
+        <!--<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>-->
+        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
+        <script src="js/libs/jquery-migrate/jquery-migrate-1.2.1.min.js"></script>
+        <!--<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-migrate/1.2.1/jquery-migrate.min.js"></script>-->
+        <script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.22/jquery-ui.min.js"></script>       
+        <script src="js/libs/timepicker/picker.min.js"></script>
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <script src="js/libs/codemirror/codemirror_combined.js"></script>
+        <script src="lib/angular-ui/fullcalendar.min.js"></script>      
+        <script src="js/libs/jsdifflib/diff_combined.min.js"></script> 
+        <link href="js/libs/jsdifflib/diffview.css" rel="stylesheet">
+        <script src="js/libs/json/json2-min.js"></script>
+        <!--<script src="js/configuration.js"></script>-->
+        <!--<script src="js/min/models.js"></script>-->
+        <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
+        <!--[if lt IE 9]>
+          <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
+        <![endif]-->
+    </head>
+    <body id="synergy_session" data-ng-controller="SynergyCtrl" >
+        <div class="navbar navbar-fixed-top" id="navbar-top" >
+            <div class="navbar-inner"  >
+                <div class="container">
+                    <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                    </a>
+                    <a class="brand" href="#" data-ng-class="busyBrand" >Synergy</a>
+                    <div class="nav-collapse" id="synergy_mainbar">
+                        <ul class="nav" id="navbar" >
+                            <li class="active" id="nav_home"><a href="#">Home</a></li>
+                            <li id="nav_runs"><a href="#/runs">Test Runs</a></li>
+                            <li id="nav_specs"><a href="#/specifications">Test Specifications</a></li>
+                            <li class="dropdown " id="nav_other" >
+                                <a href="#" class="dropdown-toggle" data-toggle="dropdown">Other <b class="caret"></b></a>
+                                <ul class="dropdown-menu">
+                                    <li><a href="#/tribes">Tribes</a></li>
+                                    <li><a href="#/calendar">Calendar</a></li>
+                                    <li class="divider"></li>
+                                    <li><a href="#/about">About</a></li>
+                                </ul>
+                            </li>
+                            <li id="nav_admin" class="dropdown" data-ng-show="role == 'admin' || role == 'manager'">
+                                <a href="#" class="dropdown-toggle" data-toggle="dropdown">Administration <b class="caret"></b></a>
+                                <ul class="dropdown-menu">
+                                    <li><a href="#/administration/runs">Test Runs</a></li>
+                                    <li><a href="#/administration/versions">Versions</a></li>
+                                    <li><a href="#/administration/platforms">Platforms</a></li>
+                                    <li><a href="#/administration/projects">Projects</a></li>
+                                    <li class="divider"></li>
+                                    <li><a href="#/administration/users">Users</a></li>
+                                    <li><a href="#/administration/tribes">Tribes</a></li>
+                                    <li><a href="#/administration/reviews">Tutorials</a></li>
+                                    <li class="divider"></li>
+                                    <li><a href="#/administration/setting">Server setting</a></li>
+                                    <li><a href="#/administration/log">View log</a></li>
+                                    <li><a href="#/administration/database">Database</a></li>
+                                </ul>
+                            </li>
+                            <li>
+                                <typeaheads items="suggestions" prompt="Search" model="searchedItem"  data-type="searchAhead()" on-select="goToSearch()"  data-enter="synergySearch()" />
+                            </li>
+                        </ul>
+                       <div class="pull-right">
+                            <button id="synergy_login_form" class="btn " data-ng-click="login();" >Sign in</button>
+                            <div id="synergy_usermenu" style="display:none">
+                                <ul class="nav pull-right"><li class="dropdown"><a href="#" class="dropdown-toggle btn-primary" data-toggle="dropdown" id="usermenu_user" style="color: white">USER <b class="caret" id="userCaret"></b></a>
+                                        <ul class="dropdown-menu">
+                                            <li><a href="#/user">Me</a></li>
+                                            <li data-ng-show="role == 'admin' || role == 'manager'"><a href="#/administration">Administration</a></li>
+                                            <li><a href="#logout" data-ng-click="logout();">Logout</a></li>
+                                        </ul></li></ul>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="container">
+
+
+            <div class="row-fluid" data-ng-cloak>
+                <div class="span12">
+                    <ul class="breadcrumb">
+                        <li><a href="#">Home</a> <span class="divider">/</span></li>
+                        <li data-ng-repeat="b in breadcrumbs"><a href="#/{{b.link}}">{{b.title}}</a> <span class="divider">/</span></li>
+                    </ul>
+                </div>
+            </div>
+            <div class="row-fluid"  >
+                <div class="span5">
+                    <div data-ng-cloak data-ng-show="SYNERGY.logger.print" class="alert {{SYNERGY.logger.style}}">
+                        <strong>{{SYNERGY.logger.title}}</strong>&nbsp;<span>{{SYNERGY.logger.msg}}</span><br/>
+                        {{SYNERGY.logger.date}}
+                    </div>
+                </div></div>
+            <div class="row-fluid"  >
+                <div class="span12">
+                    <div data-ng-view data-ng-cloak></div>
+                </div>
+                <div class="span2">
+                </div>
+            </div>
+            <hr>
+            <footer>
+                <div>
+                    <div></div> <small class="pull-right">Synergy v<span>{{SYNERGY.version}} </span> | <a data-ng-show="!SYNERGY.useSSO" href="#/register">Register</a></small>
+                </div>
+            </footer>
+
+        </div> 
+        <div class="modal hide" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+            <div class="modal-header">
+                <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+                <h3 id="myModalLabel">Modal header</h3>
+            </div>
+            <div class="modal-body" id="modal-body">
+                <p>One fine body 2…</p>
+            </div>
+            <div class="modal-footer">
+                <button class="btn btn-primary" data-dismiss="modal">OK</button>
+            </div>
+        </div>
+        <script src="js/bootstrap/bootstrap_combined.js?v=2"></script>
+        <script src="lib/angular/1.0.8/angular_combined.min.js"></script>
+        <script src="lib/angular-ui/ui-codemirror.min.js"></script>
+        <script src="lib/angular-ui/select2_combined.min.js"></script>
+        <script src="lib/bootstrap-custom/ui-bootstrap-custom-tpls-0.6.0.min.js"></script>
+        <script src="js/legacy/polyfills.js?v=1521293430023" type="text/javascript"></script>
+        <script src="js/min/synergy.js?v=1521293430023"></script>
+    </body>
+</html>
diff --git a/synergy/client/app/index2.html b/synergy/client/app/index2.html
new file mode 100755
index 0000000..2b37b8b
--- /dev/null
+++ b/synergy/client/app/index2.html
@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<html lang="en" xmlns:ng="http://angularjs.org" id="ng-app" data-ng-app="synergy" class="ng-app:synergy">
+    <head>
+        <link rel="SHORTCUT ICON" href="favicon.ico"/>
+        <meta charset="utf-8">
+        <link href="./opensearch.xml" rel="search" title="synergy search" type="application/opensearchdescription+xml">
+        <meta http-equiv="X-UA-Compatible" content="IE=10" />
+        <title>Synergy - Test Management Tool</title>
+        <link rel="stylesheet" href="lib/angular-ui/fullcalendar.min.css" >
+        <link rel="stylesheet" type="text/css" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.22/themes/base/jquery-ui.css">
+        <link href="css/min/bootstrap.css?v=1" rel="stylesheet">
+        <link href="css/min/custom.css?v=1521293430023" rel="stylesheet">
+        <link rel="stylesheet" type="text/css" href="js/libs/codemirror/codemirror.min.css">
+        <link href="css/min/bootstrap-responsive.css" rel="stylesheet">
+        <link href="lib/angular-ui/select2.min.css" rel="stylesheet">
+        <script src="js/libs/jquery-1.10.2/jquery.min.js"></script>
+        <script src="js/libs/jquery-migrate/jquery-migrate-1.2.1.min.js"></script>
+        <script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.22/jquery-ui.min.js"></script>       
+        <script src="js/libs/timepicker/picker.min.js"></script>
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <script src="js/libs/codemirror/codemirror_combined.js"></script>
+        <script src="lib/angular-ui/fullcalendar.min.js"></script>      
+        <script src="js/libs/jsdifflib/diff_combined.min.js"></script> 
+        <link href="js/libs/jsdifflib/diffview.css" rel="stylesheet">
+        <script src="js/libs/json/json2-min.js"></script>
+        <!--<script src="js/configuration.js"></script>-->
+        <!--<script src="js/min/models.js"></script>-->
+        <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
+        <!--[if lt IE 9]>
+          <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
+        <![endif]-->
+    </head>
+    <body id="synergy_session" data-ng-controller="SynergyCtrl" >
+        <div class="navbar navbar-fixed-top" id="navbar-top" >
+            <div class="navbar-inner"  >
+                <div class="container">
+                    <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                    </a>
+                    <a class="brand" href="#" data-ng-class="busyBrand" >Synergy</a>
+                    <div class="nav-collapse" id="synergy_mainbar">
+                        <ul class="nav" id="navbar" >
+                            <li class="active" id="nav_home"><a href="#">Home</a></li>
+                            <li id="nav_runs"><a href="#/runs">Test Runs</a></li>
+                            <li id="nav_specs"><a href="#/specifications">Test Specifications</a></li>
+                            <li class="dropdown " id="nav_other" >
+                                <a href="#" class="dropdown-toggle" data-toggle="dropdown">Other <b class="caret"></b></a>
+                                <ul class="dropdown-menu">
+                                    <li><a href="#/tribes">Tribes</a></li>
+                                    <li><a href="#/calendar">Calendar</a></li>
+                                    <li class="divider"></li>
+                                    <li><a href="#/about">About</a></li>
+                                </ul>
+                            </li>
+                            <li id="nav_admin" class="dropdown" data-ng-show="role == 'admin' || role == 'manager'">
+                                <a href="#" class="dropdown-toggle" data-toggle="dropdown">Administration <b class="caret"></b></a>
+                                <ul class="dropdown-menu">
+                                    <li><a href="#/administration/runs">Test Runs</a></li>
+                                    <li><a href="#/administration/versions">Versions</a></li>
+                                    <li><a href="#/administration/platforms">Platforms</a></li>
+                                    <li><a href="#/administration/projects">Projects</a></li>
+                                    <li class="divider"></li>
+                                    <li><a href="#/administration/users">Users</a></li>
+                                    <li><a href="#/administration/tribes">Tribes</a></li>
+                                    <li><a href="#/administration/reviews">Tutorials</a></li>
+                                    <li class="divider"></li>
+                                    <li><a href="#/administration/setting">Server setting</a></li>
+                                    <li><a href="#/administration/log">View log</a></li>
+                                    <li><a href="#/administration/database">Database</a></li>
+                                </ul>
+                            </li>
+                            <li>
+                                <typeaheads items="suggestions" prompt="Search" model="searchedItem"  data-type="searchAhead()" on-select="goToSearch()"  data-enter="synergySearch()" />
+                            </li>
+                        </ul>
+                       <div class="pull-right">
+                            <button id="synergy_login_form" class="btn " data-ng-click="login();" >Sign in</button>
+                            <div id="synergy_usermenu" style="display:none">
+                                <ul class="nav pull-right"><li class="dropdown"><a href="#" class="dropdown-toggle btn-primary" data-toggle="dropdown" id="usermenu_user" style="color: white">USER <b class="caret" id="userCaret"></b></a>
+                                        <ul class="dropdown-menu">
+                                            <li><a href="#/user">Me</a></li>
+                                            <li data-ng-show="role == 'admin' || role == 'manager'"><a href="#/administration">Administration</a></li>
+                                            <li><a href="#logout" data-ng-click="logout();">Logout</a></li>
+                                        </ul></li></ul>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="container">
+
+
+            <div class="row-fluid" data-ng-cloak>
+                <div class="span12">
+                    <ul class="breadcrumb">
+                        <li><a href="#">Home</a> <span class="divider">/</span></li>
+                        <li data-ng-repeat="b in breadcrumbs"><a href="#/{{b.link}}">{{b.title}}</a> <span class="divider">/</span></li>
+                    </ul>
+                </div>
+            </div>
+            <div class="row-fluid"  >
+                <div class="span5">
+                    <div data-ng-cloak data-ng-show="SYNERGY.logger.print" class="alert {{SYNERGY.logger.style}}">
+                        <strong>{{SYNERGY.logger.title}}</strong>&nbsp;<span>{{SYNERGY.logger.msg}}</span><br/>
+                        {{SYNERGY.logger.date}}
+                    </div>
+                </div></div>
+            <div class="row-fluid"  >
+                <div class="span12">
+                    <div data-ng-view data-ng-cloak></div>
+                </div>
+                <div class="span2">
+                </div>
+            </div>
+            <hr>
+            <footer>
+                <div>
+                    <div></div> <small class="pull-right">Synergy v<span>{{SYNERGY.version}}</span> | <a data-ng-show="!SYNERGY.useSSO" href="#/register">Register</a></small>
+                </div>
+            </footer>
+
+        </div> 
+        <div class="modal hide" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+            <div class="modal-header">
+                <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+                <h3 id="myModalLabel">Modal header</h3>
+            </div>
+            <div class="modal-body" id="modal-body">
+                <p>One fine body 2…</p>
+            </div>
+            <div class="modal-footer">
+                <button class="btn btn-primary" data-dismiss="modal">OK</button>
+            </div>
+        </div>
+        <script src="js/bootstrap/bootstrap_combined.js?v=21"></script>
+        <script src="lib/angular/1.0.8/angular_combined.min.js"></script>
+        <script src="lib/angular-ui/ui-codemirror.min.js"></script>
+        <script src="lib/angular-ui/select2_combined.min.js"></script>
+        <script src="lib/bootstrap-custom/ui-bootstrap-custom-tpls-0.6.0.min.js"></script>
+        <script src="js/legacy/polyfills.js?v=1521293430023" type="text/javascript"></script>
+        <script src="js/min/synergy.js?v=1521293430023"></script>
+    </body>
+</html>
diff --git a/synergy/client/app/index_dev.html b/synergy/client/app/index_dev.html
new file mode 100755
index 0000000..1821d4c
--- /dev/null
+++ b/synergy/client/app/index_dev.html
@@ -0,0 +1,179 @@
+<!DOCTYPE html>
+<html lang="en" xmlns:ng="http://angularjs.org" id="ng-app" data-ng-app="synergy" class="ng-app:synergy">
+    <head>
+        <link rel="SHORTCUT ICON" href="favicon.ico"/>
+        <meta charset="utf-8">
+        <meta http-equiv="X-UA-Compatible" content="IE=10" />
+        <title>Dev Synergy - Test Management Tool</title>
+        <link rel="stylesheet" href="lib/angular-ui/fullcalendar.min.css" >
+        <link rel="stylesheet" type="text/css" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.22/themes/base/jquery-ui.css">
+        <link href="css/min/bootstrap.css" rel="stylesheet">
+        <link href="css/custom.css" rel="stylesheet">
+        <link rel="stylesheet" type="text/css" href="js/libs/codemirror/codemirror.min.css">
+        <link href="css/min/bootstrap-responsive.css" rel="stylesheet">
+        <link href="lib/angular-ui/select2.css" rel="stylesheet">
+        <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
+        <script src="//cdnjs.cloudflare.com/ajax/libs/jquery-migrate/1.2.1/jquery-migrate.min.js"></script>
+        <script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.22/jquery-ui.min.js"></script>       
+        <script src="js/libs/timepicker/picker.min.js"></script>
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta name="description" content="">
+        <meta name="author" content="">
+        <script src="js/libs/codemirror/codemirror.min.js"></script>
+        <script src="js/libs/codemirror/xml.min.js"></script>
+        <script src="js/libs/codemirror/javascript.min.js"></script>
+        <script src="js/libs/codemirror/css.min.js"></script>
+        <script src="js/libs/codemirror/htmlmixed.min.js"></script>
+        <script src="js/libs/jquery-qrcode-master/jquery.qrcode.min.js"></script>
+        <script src="lib/angular-ui/fullcalendar.min.js"></script>     
+        <script src="js/libs/jsdifflib/diffview.js"></script> 
+        <link href="js/libs/jsdifflib/diffview.css" rel="stylesheet">
+        <script src="js/libs/jsdifflib/difflib.js"></script>      
+
+        <style type="text/css">
+            body {
+                padding-top: 60px;
+                padding-bottom: 40px;
+            }
+        </style>
+        <script src="js/libs/json/json2-min.js"></script>
+        <script src="js/exts.js"></script>
+        <style>
+
+
+        </style>
+        <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
+        <!--[if lt IE 9]>
+          <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
+        <![endif]-->
+    </head>
+    <body id="synergy_session" data-ng-controller="SynergyCtrl" >
+        <div class="navbar navbar-inverse navbar-fixed-top" id="navbar-top" >
+            <div class="navbar-inner"  >
+                <div class="container">
+                    <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                    </a>
+                    <a class="brand" href="#" data-ng-class="busyBrand" >Synergy</a>
+                    <div class="nav-collapse" id="synergy_mainbar">
+                        <ul class="nav" id="navbar" >
+                            <li class="active" id="nav_home"><a href="#">Home</a></li>
+                            <li id="nav_runs"><a href="#/runs">Test Runs</a></li>
+                            <li id="nav_specs"><a href="#/specifications">Test Specifications</a></li>
+                            <li class="dropdown " id="nav_other" >
+                                <a href="#" class="dropdown-toggle" data-toggle="dropdown">Other <b class="caret"></b></a>
+                                <ul class="dropdown-menu">
+                                    <li><a href="#/tribes">Tribes</a></li>
+                                    <li><a href="#/calendar">Calendar</a></li>
+                                    <li class="divider"></li>
+                                    <li><a href="#/about">About</a></li>
+                                </ul>
+                            </li>
+                            <li id="nav_admin" class="dropdown" data-ng-show="role == 'admin' || role == 'manager'">
+                                <a href="#" class="dropdown-toggle" data-toggle="dropdown">Administration <b class="caret"></b></a>
+                                <ul class="dropdown-menu">
+                                    <li><a href="#/administration/runs">Test Runs</a></li>
+                                    <li><a href="#/administration/versions">Versions</a></li>
+                                    <li><a href="#/administration/platforms">Platforms</a></li>
+                                    <li><a href="#/administration/projects">Projects</a></li>
+                                    <li class="divider"></li>
+                                    <li><a href="#/administration/users">Users</a></li>
+                                    <li><a href="#/administration/tribes">Tribes</a></li>
+                                    <li><a href="#/administration/reviews">Tutorials</a></li>
+                                    <li class="divider"></li>
+                                    <li><a href="#/administration/setting">Server setting</a></li>
+                                    <li><a href="#/administration/log">View log</a></li>
+                                    <li><a href="#/administration/database">Database</a></li>
+                                </ul>
+                            </li>
+                            <li>
+                            <typeaheads items="suggestions" prompt="Search" model="searchedItem"  data-type="searchAhead()" on-select="goToSearch()"  data-enter="synergySearch()" />
+                            </li>
+                        </ul>
+                        <div class="pull-right">
+                            <button id="synergy_login_form" class="btn " data-ng-click="login();" >Sign in</button>
+                            <div id="synergy_usermenu" style="display:none">
+                                <ul class="nav pull-right"><li class="dropdown"><a href="#" class="dropdown-toggle btn-primary" data-toggle="dropdown" id="usermenu_user" style="color: white">USER <b class="caret"></b></a>
+                                        <ul class="dropdown-menu">
+                                            <li><a href="#/user">Me</a></li>
+                                            <li data-ng-show="role == 'admin' || role == 'manager'"><a href="#/administration">Administration</a></li>
+                                            <li><a href="#logout" data-ng-click="logout();">Logout</a></li>
+                                        </ul></li></ul>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="container">
+
+
+            <div class="row-fluid" data-ng-cloak>
+                <div class="span12">
+                    <ul class="breadcrumb">
+                        <li><a href="index.html">Home</a> <span class="divider">/</span></li>
+                        <li data-ng-repeat="b in breadcrumbs"><a href="#/{{b.link}}">{{b.title}}</a> <span class="divider">/</span></li>
+                    </ul>
+                </div>
+            </div>
+            <div class="row-fluid"  >
+                <div class="span5">
+                    <div data-ng-cloak data-ng-show="SYNERGY.logger.print" class="alert {{SYNERGY.logger.style}}">
+                        <strong>{{SYNERGY.logger.title}}</strong>&nbsp;<span>{{SYNERGY.logger.msg}}</span><br/>
+                        {{SYNERGY.logger.date}}
+                    </div>
+                </div></div>
+            <div class="row-fluid"  >
+                <div class="span12">
+                    <div data-ng-view data-ng-cloak></div>
+                </div>
+                <div class="span2">
+                </div>
+            </div>
+            <hr>
+            <footer>
+                <div>
+                    <div></div> <small class="pull-right" data-ng-click="toggleBusyBrand()">Synergy v<span>{{SYNERGY.version}}</span> | <a data-ng-show="!SYNERGY.useSSO" href="#/register">Register</a></small>
+                </div>
+            </footer>
+
+        </div> 
+        <div class="modal hide" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+            <div class="modal-header">
+                <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+                <h3 id="myModalLabel">Modal header</h3>
+            </div>
+            <div class="modal-body" id="modal-body">
+                <p>One fine body 2…</p>
+            </div>
+            <div class="modal-footer">
+                <button class="btn btn-primary" data-dismiss="modal">OK</button>
+            </div>
+        </div>
+        <script src="js/bootstrap/bootstrap-transition.js"></script>
+        <script src="js/bootstrap/bootstrap-alert.js"></script>
+        <script src="js/bootstrap/bootstrap-typeahead.js"></script>
+        <script src="js/bootstrap/bootstrap-modal.js"></script>
+        <script src="js/bootstrap/bootstrap-dropdown.js"></script>
+        <script src="js/bootstrap/bootstrap-tooltip.js"></script>
+        <script src="js/bootstrap/bootstrap-collapse.js"></script>
+        <script src="lib/angular/angular.js"></script>
+        <script src="lib/angular/angular-cookies.js"></script>
+        <script src="lib/angular-ui/ui-codemirror.js"></script>
+        <script src="lib/angular-ui/select2_combined.min.js"></script>
+        <script src="lib/bootstrap-custom/ui-bootstrap-custom-tpls-0.6.0.min.js"></script>
+        <script src="js/legacy/polyfills.js" type="text/javascript"></script>
+        <script src="js/app.js"></script>
+        <script src="js/models.js" type="text/javascript"></script>
+        <script src="js/utils.js" type="text/javascript"></script>
+        <script src="js/configuration.js"></script>
+        <script src="js/handlers.js" type="text/javascript"></script>
+        <script src="js/filters.js" type="text/javascript"></script>
+        <script src="js/controllers.js"></script>
+        <script src="js/factories.js"></script>
+        <script src="js/directives.js"></script>        
+    </body>
+</html>
diff --git a/synergy/client/app/js/app.js b/synergy/client/app/js/app.js
new file mode 100755
index 0000000..ccc06f7
--- /dev/null
+++ b/synergy/client/app/js/app.js
@@ -0,0 +1,260 @@
+"use strict";
+
+
+angular.module("synergy", ["ui.codemirror",
+    "infinite-scroll",
+    "ui.select2",
+    "ngCookies",
+    "ngProgress",
+    "ui.bootstrap",
+    "synergy.http",
+    "synergy.test",
+    "synergy.core",
+    "synergy.controllers",
+    "synergy.directives",
+    "synergy.models",
+    "synergy.utils",
+    "synergy.handlers",
+    "synergy.filters"])
+        .config(function ($provide, $routeProvider, $httpProvider) {
+
+            $provide.factory("MyHttpInterceptor", ["$q", "$rootScope", "SynergyApp", "$injector", "$cookieStore", "sessionService",
+                function ($q, $rootScope, SynergyApp, $injector, $cookieStore, sessionService) { // TODO in case of AngularJS update, this is likely obsolete
+
+                    var IGNORE_URLS = ["../../server/api/login.php", "../../server/api/login.php?&return=1"];
+                    var refreshPromise = null;
+                    function SessionRenewal() {
+                        this.originalResponse = null;
+                        this.intervalId = -1;
+                        this.TOKEN_LENGTH = 32;
+                        this.token = null;
+                        this.counter = 0;
+                    }
+
+                    SessionRenewal.prototype.getToken = function () {
+                        var text = "";
+                        var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+                        for (var i = 0; i < this.TOKEN_LENGTH; i++) {
+                            text += possible.charAt(Math.floor(Math.random() * possible.length));
+                        }
+                        return text + new Date().getTime();
+                    };
+                    SessionRenewal.prototype.reset = function () {
+                        window.clearInterval(this.intervalId);
+                        this.counter = 0;
+                        this.token = null;
+                        this.intervalId = -1;
+                        this.originalResponse = null;
+                    };
+                    SessionRenewal.prototype.checkForNewLogin = function () {
+                        this.counter++;
+                        var self = this;
+
+                        $injector.get("$http").get(SynergyApp.getApp().server.buildURL("refresh", {"token": self.token})).success(function (result) {
+                            window.clearInterval(self.intervalId);
+                            self.reset();
+                            refreshPromise.resolve(self.originalResponse);
+                        }).error(function (data, status, headers, config) {
+                            if (self.counter > 100) {
+                                self.reset();
+                                refreshPromise.reject(self.originalResponse);
+                            }
+                        });
+                    };
+                    SessionRenewal.prototype.getRedirectUrl = function () {
+                        var url = window.location.href.substring(0, window.location.href.indexOf("#"));
+                        url = url.endsWith(".html") ? url.substring(0, url.lastIndexOf("/") + 1) : url;
+                        this.token = this.getToken();
+                        url += "login.html?token=" + this.token;
+                        if (SynergyApp.getApp().useSSO) {
+                            var base = SynergyApp.getApp().getLoginRedirectUrl(SynergyApp.getApp().ssoLoginUrl, url);
+                            return base.substring(0, base.indexOf("?revalidate=1"));
+                        } else {
+                            // todo fix, std login page is synergy/client/app/#/login, which is not visible for server though...
+                            throw new Error("Not implemented");
+                        }
+                    };
+                    SessionRenewal.prototype.init = function (response) {
+                        refreshPromise = $q.defer();
+
+                        this.originalResponse = response;
+                        $("#myModalLabel").text("Please login");
+                        $("#modal-body").html("Your session has expired. <br/><a href='" + this.getRedirectUrl() + "' target='_blank'>Click to login</a>");
+                        if (!$("#myModal").hasClass("in")) {
+                            $("#myModal").modal("toggle");
+                        }
+                        var self = this;
+                        this.intervalId = window.setInterval(function () {
+                            self.checkForNewLogin();
+                        }, 2000);
+
+                        return refreshPromise.promise;
+                    };
+
+                    SessionRenewal.prototype.displayDialog = function (isVisible) {
+                        if ($("#myModal").hasClass("in")) {
+                            $("#myModal").modal("toggle");
+                        }
+                    };
+
+                    SessionRenewal.prototype.renew = function (response) {
+                        var self = this;
+                        return this.init(response)
+                                .then(function () {
+                                    return $injector.get("sessionHttp").infoConditionalPromise(SynergyApp.getApp().server.buildURL("session"));
+                                })
+                                .then(function (data) {
+                                    SynergyApp.getApp().session.setSession(data.data);
+                                    sessionService.setSession(SynergyApp.getApp().session);
+                                    $cookieStore.put("session", ({firstName: data.firstName, lastName: data.lastName, username: data.username, role: data.role, token: data.token, created: 1000 * parseInt(data.created, 10), session_id: data.session_id}));
+                                    $rootScope.$broadcast("refreshRole");
+                                    self.displayDialog(false);
+                                    var $h = $injector.get("$http");
+                                    return $h(response.config);
+                                }, function () {
+                                    $cookieStore.remove("session");
+                                    sessionService.clearSession();
+                                    SynergyApp.getApp().session.clearSession();
+                                    $rootScope.$broadcast("hideUserMenu", false);
+                                    $rootScope.$broadcast("refreshRole");
+                                    self.displayDialog(false);
+                                    return $q.reject(response);
+                                });
+                    };
+
+                    var sessionRenewal = new SessionRenewal();
+
+                    return function (promise) {
+                        return promise.then(function (response) {
+                            $rootScope.$broadcast("busyMode", false);
+                            window.document.body.style.cursor = "default";
+                            return response;
+                        }, function (response) {
+                            $rootScope.$broadcast("busyMode", false);
+                            window.document.body.style.cursor = "default";
+                            if (parseInt(response.status, 10) === 0) {
+                                $rootScope.$broadcast("possibleTimeout", false);
+                            }
+                            if (parseInt(response.status, 10) === 401 && IGNORE_URLS.indexOf(response.config.url) < 0) { // expired session
+                                return sessionRenewal.renew(response);
+                            } else {
+                                return $q.reject(response);
+                            }
+                        });
+                    };
+                }]);
+
+            ($httpProvider.interceptors) ? $httpProvider.interceptors.push("MyHttpInterceptor") : $httpProvider.responseInterceptors.push("MyHttpInterceptor");
+
+            $provide.factory("utils", function () {
+                var u = {};
+                u.escape = function (s) {
+                    return encodeURIComponent(s);
+                };
+                return u;
+            });
+
+            $provide.factory("issue", function ($http) {
+                return {
+                    getIssue: function (id, $scope) { // not elegant way, $scope should be injected not from function call
+                        $http.get($scope.SYNERGY.server.buildURL("issue", {"id": id})).success(function (data) {
+                            return data;
+                        }).error(function (data) {
+                            window.console.log("Issue " + id + " not found: " + data);
+                        });
+                    }
+                };
+            });
+
+            // PUBLIC ROUTING
+            $routeProvider.when("/specifications/:id", {templateUrl: "partials/public/view/specpool.html?v=1521293430023", controller: "SpecPoolCtrl"});
+            $routeProvider.when("/statistics/:id", {templateUrl: "partials/public/view/statistics.html?v=1521293430023", controller: "StatisticsCtrl"});
+            $routeProvider.when("/statistics/:id/archive", {templateUrl: "partials/public/view/statistics.html?v=1521293430023", controller: "StatisticsCtrl"});
+            $routeProvider.when("/search/:search", {templateUrl: "partials/public/view/search.html?v=1521293430023", controller: "SearchCtrl"});
+            $routeProvider.when("/specifications", {templateUrl: "partials/public/view/specpool.html?v=1521293430023", controller: "SpecPoolCtrl"});
+            $routeProvider.when("/specification/:id/create", {templateUrl: "partials/public/create/specification.html?v=1521293430023", controller: "SpecificationCtrl"});
+            $routeProvider.when("/specification/:id/edit", {templateUrl: "partials/public/edit/specification.html?v=1521293430023", controller: "SpecificationCtrl"});
+            $routeProvider.when("/specification/:id/v/1", {templateUrl: "partials/public/view/specification_view_1.html?v=1521293430023", controller: "SpecificationCtrl"});
+            $routeProvider.when("/specification/:id/v/2", {templateUrl: "partials/public/view/specification_view_2.html?v=1521293430023", controller: "SpecificationCtrl"});
+            $routeProvider.when("/specification/:id/v/2/:label", {templateUrl: "partials/public/view/specification_view_2.html?v=1521293430023", controller: "SpecificationCtrl"});
+            $routeProvider.when("/title/:simpleName/:simpleVersion", {templateUrl: "partials/public/view/specification_view_2.html?v=1521293430023", controller: "SpecificationCtrl"});
+            $routeProvider.when("/title/:simpleName/", {redirectTo: "/title/:simpleName/latest"});
+            $routeProvider.when("/specification/:id", {redirectTo: "/specification/:id/v/2"});
+            $routeProvider.when("/suite/:id/:specification/:version/create", {templateUrl: "partials/public/create/suite.html?v=1521293430023", controller: "SuiteCtrl"});
+            $routeProvider.when("/suite/:id/edit", {templateUrl: "partials/public/edit/suite.html?v=1521293430023", controller: "SuiteCtrl"});
+            $routeProvider.when("/suite/:id/v/1", {templateUrl: "partials/public/view/suite.html?v=1521293430023", controller: "SuiteCtrl"});
+            $routeProvider.when("/suite/:id", {redirectTo: "/suite/:id/v/1"});
+            $routeProvider.when("/case/:id/suite/:parent/edit", {templateUrl: "partials/public/edit/case.html?v=1521293430023", controller: "CaseCtrl"});
+            $routeProvider.when("/case/:id/suite/:parent/create", {templateUrl: "partials/public/create/case.html?v=1521293430023", controller: "CaseCtrl"});
+            $routeProvider.when("/case/:id/suite/:parent/v/1", {templateUrl: "partials/public/view/case.html?v=1521293430023", controller: "CaseCtrl"});
+            $routeProvider.when("/assignment_comments/:id", {templateUrl: "partials/public/view/assignment_comments.html?v=1521293430023", controller: "AssignmentCommentsCtrl"});
+            $routeProvider.when("/case/:id/suite/:parent", {redirectTo: "/case/:id/suite/:parent/v/1"});
+            $routeProvider.when("/case/:id", {redirectTo: "/case/:id/suite/-1/v/1"});
+            $routeProvider.when("/run/:id/v/1", {templateUrl: "partials/public/view/run_view_1.html?v=1521293430023", controller: "RunCtrl"});
+            $routeProvider.when("/run/:id/coverage", {templateUrl: "partials/public/view/run_coverage.html?v=1521293430023", controller: "RunCoverageCtrl"});
+            $routeProvider.when("/run/:id/v/2", {templateUrl: "partials/public/view/run_view_2.html?v=1521293430023", controller: "RunCtrlUser"});
+            $routeProvider.when("/run/:id/v/3", {templateUrl: "partials/public/view/run_view_3.html?v=1521293430023", controller: "RunCtrlCase"});
+            $routeProvider.when("/run/:id", {redirectTo: "/run/:id/v/2"});
+            $routeProvider.when("/runs/page/:page", {templateUrl: "partials/public/view/runs.html?v=1521293430023", controller: "RunsCtrl"});
+            $routeProvider.when("/runs", {redirectTo: "/runs/page/1"});
+            $routeProvider.when("/user", {templateUrl: "partials/public/view/profile.html?v=1521293430023", controller: "ProfileCtrl"});
+            $routeProvider.when("/user/:user", {templateUrl: "partials/public/view/profile.html?v=1521293430023", controller: "ProfileCtrl"});
+            $routeProvider.when("/label/:label/page/:page", {templateUrl: "partials/public/view/label.html?v=1521293430023", controller: "LabelFilterCtrl"});
+            $routeProvider.when("/label/:label", {redirectTo: "/label/:label/page/1"});
+            $routeProvider.when("/tribe/:id/edit", {templateUrl: "partials/public/edit/tribe.html?v=1521293430023", controller: "TribeCtrl"});
+            $routeProvider.when("/tribe/:id/view", {templateUrl: "partials/public/view/tribe.html?v=1521293430023", controller: "TribeCtrl"});
+            $routeProvider.when("/tribes", {templateUrl: "partials/public/view/tribes.html?v=1521293430023", controller: "TribesCtrl"});
+            $routeProvider.when("/calendar", {templateUrl: "partials/public/view/calendar.html?v=1521293430023", controller: "CalendarCtrl"});
+            $routeProvider.when("/tribe/:id", {redirectTo: "/tribe/:id/view"});
+            $routeProvider.when("/", {templateUrl: "partials/public/view/home.html?v=1521293430023", controller: "HomeCtrl"});
+            $routeProvider.when("/assignment/:id/v/:mode", {templateUrl: "partials/public/view/assignment.html?v=1521293430023", controller: "AssignmentCtrl"});
+            $routeProvider.when("/assignment/create/run/:id", {templateUrl: "partials/public/create/assignment.html?v=1521293430023", controller: "AssignmentVolunteerCtrl"});
+            $routeProvider.when("/assignment/create/tribe/run/:id", {templateUrl: "partials/public/create/assignment_tribe.html?v=1521293430023", controller: "AssignmentTribeCtrl"});
+            $routeProvider.when("/about", {templateUrl: "partials/public/view/about.html?v=1521293430023", controller: "AboutCtrl"});
+            $routeProvider.when("/revisions/:id", {templateUrl: "partials/public/view/revisions.html?v=1521293430023", controller: "RevisionCtrl"});
+            $routeProvider.when("/review/:id/view", {templateUrl: "partials/public/view/review.html?v=1521293430023", controller: "ReviewCtrl"});
+            $routeProvider.when("/review/:id/:action", {templateUrl: "partials/public/edit/review.html?v=1521293430023", controller: "ReviewCtrl"});
+            $routeProvider.when("/register", {templateUrl: "partials/public/view/register.html?v=1521293430023", controller: "RegisterCtrl"});
+            $routeProvider.when("/login", {templateUrl: "partials/public/view/login.html?v=1521293430023", controller: "LoginCtrl"});
+            $routeProvider.when("/recover", {templateUrl: "partials/public/view/recover.html?v=1521293430023", controller: "RecoverCtrl"});
+
+            // ADMINSTRATION ROUTING
+            $routeProvider.when("/administration", {templateUrl: "partials/admin/view/home.html?v=1521293430023", controller: "AdminHomeCtrl"});
+            $routeProvider.when("/administration/versions", {templateUrl: "partials/admin/view/versions.html?v=1521293430023", controller: "AdminVersionCtrl"});
+            $routeProvider.when("/administration/runs/page/:page", {templateUrl: "partials/admin/view/runs.html?v=1521293430023", controller: "AdminRunsCtrl"});
+            $routeProvider.when("/administration/run/-1/create", {templateUrl: "partials/admin/create/run.html?v=1521293430023", controller: "AdminRunCtrl"});
+            $routeProvider.when("/administration/assignment/create/run/:id", {templateUrl: "partials/admin/create/assignment.html?v=1521293430023", controller: "AdminAssignmentCtrl"});
+            $routeProvider.when("/administration/assignment/creatematrix/run/:id", {templateUrl: "partials/admin/create/matrix_assignment.html?v=1521293430023", controller: "AdminMatrixAssignmentCtrl"});
+            $routeProvider.when("/administration/run/:id/edit", {templateUrl: "partials/admin/edit/run.html?v=1521293430023", controller: "AdminRunCtrl"});
+            $routeProvider.when("/administration/runs", {redirectTo: "/administration/runs/page/1"});
+            $routeProvider.when("/administration/tribes/create", {templateUrl: "partials/admin/create/tribe.html?v=1521293430023", controller: "AdminTribesCtrl"});
+            $routeProvider.when("/administration/tribes", {templateUrl: "partials/admin/view/tribes.html?v=1521293430023", controller: "AdminTribesCtrl"});
+            $routeProvider.when("/administration/platforms", {templateUrl: "partials/admin/view/platforms.html?v=1521293430023", controller: "AdminPlatformsCtrl"});
+            $routeProvider.when("/administration/users/page/:page", {templateUrl: "partials/admin/view/users.html?v=1521293430023", controller: "AdminUsersCtrl"});
+            $routeProvider.when("/administration/user/:username/edit", {templateUrl: "partials/admin/edit/user.html?v=1521293430023", controller: "AdminUserCtrl"});
+            $routeProvider.when("/administration/user/:username/create", {templateUrl: "partials/admin/create/user.html?v=1521293430023", controller: "AdminUserCtrl"});
+            $routeProvider.when("/administration/users", {redirectTo: "/administration/users/page/1"});
+            $routeProvider.when("/administration/setting", {templateUrl: "partials/admin/view/settings.html?v=1521293430023", controller: "AdminSettingCtrl"});
+            $routeProvider.when("/administration/log", {templateUrl: "partials/admin/view/log.html?v=1521293430023", controller: "AdminLogCtrl"});
+            $routeProvider.when("/administration/database", {templateUrl: "partials/admin/view/database.html?v=1521293430023", controller: "AdminDatabaseCtrl"});
+            $routeProvider.when("/administration/reviews", {templateUrl: "partials/admin/view/reviews.html?v=1521293430023", controller: "AdminReviewsCtrl"});
+            $routeProvider.when("/administration/projects", {templateUrl: "partials/admin/view/projects.html?v=1521293430023", controller: "AdminProjectsCtrl"});
+            $routeProvider.when("/administration/project/:id/edit", {templateUrl: "partials/admin/edit/project.html?v=1521293430023", controller: "AdminProjectCtrl"});
+            $routeProvider.otherwise({redirectTo: "/"});
+
+        }).run(["$rootScope", "$injector", "sessionService", function ($rootScope, $injector, sessionService) {
+        $injector.get("$http").defaults.transformRequest = function (data, headersGetter) {
+            var _t = sessionService.getToken();
+            if (_t) {
+                headersGetter()["Synergy-Authorization"] = _t;
+            }
+            return data;
+        };
+        /*
+         Receive emitted message and broadcast it.
+         Event names must be distinct or browser will blow up!
+         */
+        $rootScope.$on("handleEmit", function (event, args) {
+            $rootScope.$broadcast("handleBroadcast", args);
+        });
+    }]);
\ No newline at end of file
diff --git a/synergy/client/app/js/configuration.js b/synergy/client/app/js/configuration.js
new file mode 100755
index 0000000..b2833b4
--- /dev/null
+++ b/synergy/client/app/js/configuration.js
@@ -0,0 +1,393 @@
+"use strict";
+(function () {
+    /**
+     * 
+     * Container for synergy configuration
+     * @type Synergy
+     */
+    function Synergy(SynergyHandlers) {
+        var synergy = this;
+        this.version = "1.0.12";
+        this.hostname = window.location.hostname;
+        this.baseURL = this.hostname + "/synergy";
+        this.bugtrackingSystems = {};
+        this.issues = new function () {
+            this.singleIssueLink = function (project, issue, includeText) {
+                if (synergy.bugtrackingSystems.hasOwnProperty(project) && typeof synergy.bugtrackingSystems[project].getDisplayLink === "function") {
+                    return synergy.bugtrackingSystems[project].getDisplayLink(issue, includeText);
+                }
+                return "";
+            };
+            this.viewLinkObjects = function (project, issues, includeText) {
+                if (issues instanceof Array) {
+                    return this.viewLink(project, issues);
+                }
+                var _all = [];
+                for (var i in issues) {
+                    if (issues.hasOwnProperty(i)) {
+                        _all.push(issues[i]);
+                    }
+                }
+                return this.viewLink(project, _all, includeText);
+            };
+
+            this.viewLink = function (project, issues, includeText) {
+                if (synergy.bugtrackingSystems.hasOwnProperty(project) && typeof synergy.bugtrackingSystems[project].getMultiDisplayLink === "function") {
+                    return synergy.bugtrackingSystems[project].getMultiDisplayLink(issues, includeText);
+                }
+                return "";
+            };
+            this.reportLink = function (project, product, component, version, summary, caseId, suiteId) {
+                if (synergy.bugtrackingSystems.hasOwnProperty(project) && typeof synergy.bugtrackingSystems[project].getReportLink === "function") {
+                    return synergy.bugtrackingSystems[project].getReportLink(product, component, version, summary, caseId, suiteId);
+                }
+                return "";
+            };
+        };
+
+        this.assignmentPage = 20;
+        this.commentsPage = 30;
+        this.adminRoles = ["admin", "manager"];
+        this.publisher = new SynergyHandlers.SynergyObserver();
+        this.logger = new SynergyHandlers.SynergyLogger();
+        this.httpTimeout = 60000;
+        /**
+         * Fallback for backward compatibility (in prev. versions of Synergy, specification didn't have project property
+         * @type String
+         */
+        this.product = "NetBeans";
+
+        /**
+         * If true, Synergy will track how long was given case being tested and update estimated case duration with this value after submitting
+         * @type Boolean
+         */
+        this.trackCaseDuration = true;
+
+        /**
+         * Default number of miliseconds before cookie is expired
+         * @type Number
+         */
+        this.defaultCookiesExpiration = 12 * 60 * 60 * 1000;
+        this.uploadFileLimit = 20000000;
+        /**
+         * For dialogs. Properties modal, modalBody and modalHeader are IDs (with #) of elements in modal div
+         * Sample usage: $scope.SYNERGY.modal.update("Login failed", "Incorrect credentials, please try again" + data.toString());
+         $scope.SYNERGY.modal.show();
+         * @type type
+         */
+        this.modal = {
+            modal: "#myModal",
+            modalBody: "#modal-body",
+            modalHeader: "#myModalLabel",
+            update: function (header, body) {
+                $(this.modalHeader).text(header);
+                $(this.modalBody).text(body);
+            },
+            show: function () {
+                $(this.modal).modal("toggle");
+            }
+        };
+
+
+        this.server = new SynergyHandlers.SynergyServer({
+            "db": "../../server/api/db.php",
+            "specifications": "../../server/api/specifications.php",
+            "specification": "../../server/api/specification.php",
+            "session": "../../server/api/login.php",
+            "assignment": "../../server/api/assignment.php",
+            "assignments": "../../server/api/assignments.php",
+            "assignment_bugs": "../../server/api/assignment_bugs.php",
+            "tribe_assignments": "../../server/api/tribe_assignments.php",
+            "attachment": "../../server/api/attachment.php",
+            "attachments": "../../server/api/attachments.php",
+            "run_attachment": "../../server/api/run_attachment.php",
+            "favorites": "../../server/api/favorites.php",
+            "favorite": "../../server/api/favorite.php",
+            "suite": "../../server/api/suite.php",
+            "case": "../../server/api/case.php",
+            "cases": "../../server/api/cases.php",
+            "job": "../../server/api/job.php",
+            "label": "../../server/api/label.php",
+            "labels": "../../server/api/labels.php",
+            "log": "../../server/api/log.php",
+            "user": "../../server/api/user.php",
+            "profile_img": "../../server/api/profile_img.php",
+            "users": "../../server/api/users.php",
+            "tribe": "../../server/api/tribe.php",
+            "tribe_specification": "../../server/api/tribe_specification.php",
+            "tribes": "../../server/api/tribes.php",
+            "versions": "../../server/api/versions.php",
+            "version": "../../server/api/version.php",
+            "platform": "../../server/api/platform.php",
+            "platforms": "../../server/api/platforms.php",
+            "runs": "../../server/api/runs.php",
+            "image": "../../server/api/image.php",
+            "images": "../../server/api/images.php",
+            "issue": "../../server/api/issue.php",
+            "proxy": "../../server/api/proxy.php",
+            "run": "../../server/api/run.php",
+            "run_notifications": "../../server/api/run_notifications.php",
+            "events": "../../server/api/events.php",
+            "configuration": "../../server/api/configuration.php",
+            "about": "../../server/api/about.php",
+            "sanitizer": "../../server/api/sanitizer.php",
+            "search": "../../server/api/search.php",
+            "products": "../../server/api/products.php",
+            "revisions": "../../server/api/revisions.php",
+            "statistics": "../../server/api/statistics.php",
+            "statistics_archived": "../../server/data/test_runs/",
+            "statistics_fallback": "../../server/archive/test_runs_data/",
+            "statistics_filter": "../../server/api/statistics_filter.php",
+            "comments": "../../server/api/comments.php",
+            "assignment_comments": "../../server/api/assignment_comments.php",
+            "specification_request": "../../server/api/specification_request.php",
+            "versionLength": "../../server/api/specification_length.php",
+            "assignment_exists": "../../server/api/assignment_exists.php",
+            "review": "../../server/api/review.php",
+            "review_assignment": "../../server/api/review_assignment.php",
+            "reviews": "../../server/api/reviews.php",
+            "projects": "../../server/api/projects.php",
+            "project": "../../server/api/project.php",
+            "register": "../../server/api/register.php",
+            "run_specifications": "../../server/api/run_specifications.php",
+            "run_tribes": "../../server/api/run_tribes.php",
+            "refresh": "../../server/api/refresh.php"
+        });
+        /**
+         * To specify server endpoints used by Synergy Client
+         * 
+         */
+
+
+        /**
+         * Holds information about current session and modifies page upon session state
+         */
+        this.session = {
+            isLoggedIn: false,
+            username: "",
+            firstName: "",
+            lastName: "",
+            role: "",
+            created: "",
+            session_id: "",
+            token: "",
+            cookieIsValid: function (creationTime) {
+                return ((new Date().getTime() - parseInt(creationTime, 10)) < synergy.defaultCookiesExpiration);
+            },
+            /**
+             * Hides login form after successful login
+             * 
+             */
+            hideLoginForm: function () {
+                $("#synergy_login_form").css("display", "none");
+                $("#synergy_login_form_log").css("display", "none");
+            },
+            clearSession: function () {
+                synergy.session.isLoggedIn = false;
+                synergy.session.username = "";
+                synergy.session.lastName = "";
+                synergy.session.firstName = "";
+                synergy.session.role = "";
+                synergy.session.created = -1;
+                synergy.session.session_id = "";
+                synergy.session.token = "";
+                synergy.session.showLoginForm();
+                synergy.session.hideUserMenu();
+            },
+            setSession: function (data) {
+                synergy.session.hideLoginForm();
+                synergy.session.showUserMenu(data.username);
+                synergy.session.isLoggedIn = true;
+                synergy.session.username = data.username;
+                synergy.session.lastName = data.lastName;
+                synergy.session.firstName = data.firstName;
+                synergy.session.role = data.role;
+                synergy.session.session_id = data.session_id;
+                synergy.session.token = data.session_id;
+                synergy.session.created = data.created;
+            },
+            showUserMenu: function (username) {
+                $("#usermenu_user").html(username + "&nbsp;<b class=\"caret\" id=\"userCaret\"></b>");
+                $("#synergy_usermenu").css("display", "block");
+            },
+            showLoginForm: function () {
+                $("#synergy_login_form").css("display", "block");
+            },
+            hideUserMenu: function () {
+                $("#synergy_usermenu").css("display", "none");
+            },
+            hasAdminRights: function () {
+                if (typeof synergy.session !== "undefined" && typeof synergy.session.role !== "undefined" && synergy.adminRoles.indexOf(synergy.session.role) > -1) {
+                    return true;
+                } else {
+                    return false;
+                }
+            },
+            /**
+             * Displays user menu that is shown if user is logged in
+             * 
+             */
+            createUserMenu: function () {
+                $("#synergy_session").append("<ul class=\"nav pull-right\"><li class=\"dropdown\"><a href=\"#\" class=\"dropdown-toggle btn-primary\" data-toggle=\"dropdown\" style=\"color: white\">" + synergy.session.username + " <b class=\"caret\"></b></a>" +
+                        "<ul class=\"dropdown-menu\"><li><a href=\"#favorites\">Favorites</a></li><li><span ng-click=\"logout();\">Logout</span></li>" +
+                        "</ul></li></ul>");
+            }
+        };
+
+        /**
+         * Some useful functions
+         */
+        this.util = {
+            /**
+             * Converts associated array (object) to indexed based array
+             * @param {type} data
+             * @returns {Array|Synergy.util.toIndexedArray._a}
+             */
+            toIndexedArray: function (data) {
+                var _a = [];
+                for (var i in data) {
+                    if (data.hasOwnProperty(i)) {
+                        _a.push(data[i]);
+                    }
+                }
+                return _a;
+            },
+            /**
+             * Sets cookie value
+             * @param {type} name
+             * @param {type} value
+             */
+            setCookie: function (name, value) {
+                var date = new Date();
+                date.setTime(date.getTime() + (synergy.defaultCookiesExpiration));
+                var expires = "; expires=" + date.toGMTString();
+                window.document.cookie = name + "=" + value + expires + "; path=/";
+            },
+            /**
+             * Scrolls window so the beginning of element with given ID is visible in viewport
+             * @param {type} elementID
+             * @returns {undefined}
+             */
+            scrollTo: function (elementID) {
+                var positionX = 0;
+                var positionY = 0;
+                var navbar = window.document.getElementById("navbar-top");
+                var element = window.document.getElementById(elementID);
+                while (element !== null) {
+                    positionX += element.offsetLeft;
+                    positionY += element.offsetTop;
+                    element = element.offsetParent;
+                }
+                window.scrollTo(positionX, positionY - navbar.offsetHeight);
+            },
+            /**
+             * Returns cookie value
+             * @param {type} name
+             * @returns {unresolved}
+             */
+            getCookie: function (name) {
+                name += "=";
+                var ca = window.document.cookie.split(";");
+                for (var i = 0; i < ca.length; i++) {
+                    var c = ca[i];
+                    while (c.charAt(0) === " ") {
+                        c = c.substring(1, c.length);
+                    }
+                    if (c.indexOf(name) === 0) {
+                        return c.substring(name.length, c.length);
+                    }
+                }
+            },
+            /**
+             * Deletes cookie
+             * @param {type} name
+             * @returns {undefined}
+             */
+            deleteCookie: function (name) {
+                window.document.cookie = name + "=;; path=/";
+            },
+            encodeHTML: function () {
+                // encodes all <pre> tags
+                var pre = $("pre");
+                pre.html($("<div/>").text(pre.html).html());
+
+            }
+
+
+        };
+
+        /**
+         * Manipulates with cache used in Synergy. This implementation relies on localStorage
+         */
+        this.cache = {
+            /**
+             * If value with given key exists, updates it. Otherwise new record is stored in localstorage
+             */
+            "put": function (key, value) {
+                if (window.localStorage) {
+                    window.localStorage.removeItem(key);
+                    try {
+                        window.localStorage.setItem(key, JSON.stringify(value));
+                    } catch (e) {
+                        if (e.code === 22 || e.code === 21 || e.code === 20) {
+                            this.drop();
+                            window.localStorage.setItem(key, JSON.stringify(value));
+                        }
+                    }
+                }
+            },
+            /**
+             * Removes everything from localStorage
+             * @returns {undefined}
+             */
+            "drop": function () {
+                for (var i = 0, max = window.localStorage.length; i < max; i++) {
+                    window.localStorage.removeItem(window.localStorage.key(0));
+                }
+            },
+            "clear": function (key) {
+                if (window.localStorage) {
+                    window.localStorage.removeItem(key);
+                }
+            },
+            "get": function (key) {
+                if (window.localStorage) {
+                    return JSON.parse(window.localStorage.getItem(key));
+                }
+                return null;
+            }
+
+        };
+
+        /**
+         * URL where should Synergy redirect you to login
+         */
+        this.ssoLoginUrl = "https://netbeans.org/people/login?original_uri=";
+        this.ssoLogoutUrl = "https://netbeans.org/people/logout?original_uri=";
+        this.getLoginRedirectUrl = function (loginUrl, redirectUrl) {
+            loginUrl += encodeURI(redirectUrl);
+            return loginUrl + "?revalidate=1";
+        };
+        this.getLogoutRedirectUrl = function (logoutUrl, redirectUrl) {
+            logoutUrl += encodeURI(redirectUrl);
+            return logoutUrl + "?revalidate=1";
+        };
+
+        this.useSSO = false;
+    }
+
+    angular.module("synergy.core", ["synergy.handlers"])
+            .factory("SynergyCore", ["SynergyHandlers", function (SynergyHandlers) {
+                    var _appCore = null;
+                    return {
+                        init: function () {
+                            if (_appCore !== null) {
+                                throw new Error("Application already initialized");
+                            } else {
+                                _appCore = new Synergy(SynergyHandlers);
+                                return _appCore;
+                            }
+                        }
+                    };
+                }]);
+})();
\ No newline at end of file
diff --git a/synergy/client/app/js/controllers.js b/synergy/client/app/js/controllers.js
new file mode 100755
index 0000000..33bcdf0
--- /dev/null
+++ b/synergy/client/app/js/controllers.js
@@ -0,0 +1,7283 @@
+"use strict";
+
+(function () {
+    function SampleSubscriber($scope, data, event) {
+//    window.console.log("Received: " + event);
+    }
+    /**
+     * Top level main controller. All other controllers are nested and have access to this $scope.
+     * Handles all session related features - log in, log out, update session information in
+     * SYNERGY.session and hides/displays login form
+     * @param {SessionFct} sessionHttp description
+     */
+    function SynergyCtrl($scope, $location, sessionHttp, $cookieStore, $timeout, ngProgress, $templateCache, searchHttp, projectsHttp, sessionService, SynergyApp, specificationCache, SynergyUtils, SynergyCore) {
+        $scope.SYNERGY = SynergyCore.init();
+        SynergyApp.setApp($scope.SYNERGY);
+        specificationCache.resetCurrentSpecification();
+        $scope.suggestions = [];
+        $scope.SYNERGY.publisher.subscribe(function ($scope, data, event) {
+            new SampleSubscriber($scope, data, event);
+        }, "specListLoaded");
+        $scope.location = $location;
+        $scope.$on("$routeChangeStart", function (scope, next, current) { // to hide alert box whenever path is changed
+            $scope.SYNERGY.logger.print = false;
+        });
+
+        $scope.username = "";
+        $scope.password = "";
+        $scope.searchedItem = "";
+        $scope.role = "";
+        $scope.cookieChecked = false; // need to check at least once in case PHPSESSID cookie is different in Synergy.cookie
+        $scope.isLoggedIn = false;
+        $scope.breadcrumbs = [];
+        $scope.busyBrand = "";
+
+        /**
+         * Shows generic dialog asking user to wait
+         */
+        $scope.showWaitDialog = function () {
+            $scope.SYNERGY.modal.update("Processing data...", "Please wait");
+            $scope.SYNERGY.modal.show();
+        };
+
+        /**
+         * Makes Synergy logo glowing (and turning off)
+         * @returns {undefined}
+         */
+        $scope.toggleBusyBrand = function () {
+            $scope.busyBrand = $scope.busyBrand.length > 0 ? "" : "brand_busy";
+        };
+
+        $scope.getLocalTime = function (dateString, useShortMonth) {
+            return SynergyUtils.UTCToLocal(dateString, useShortMonth);
+        };
+        $scope.getDate = function (dateString) {
+            return SynergyUtils.UTCToDate(dateString);
+        };
+        $scope.getLocalDateTime = function (dateString) {
+            return SynergyUtils.UTCToLocalDateTime(dateString);
+        };
+        $scope.getUTCTime = function (dateString) {
+            return SynergyUtils.localToUTC(dateString);
+        };
+
+        /**
+         * General method to show & log with level INFO message from HTTP Factory
+         * @param {type} data
+         * @param {type} status HTTP status     
+         */
+        $scope.generalHttpFactoryError = function (data, status) {
+            $scope.SYNERGY.logger.log("Action failed", data, "INFO", "alert-error");
+        };
+
+        /**
+         * Turns busy mode on, meaning logo is glowing and displays progress bar at the top of the page
+         */
+        $scope.busyModeOn = function () {
+            $scope.busyBrand = "brand_busy";
+            ngProgress.set(0);
+            $timeout(function () {
+                if (ngProgress.status() < 90) {
+                    ngProgress.set(ngProgress.status() + Math.round(Math.random() * (10 - 5 + 1) + 5));
+                }
+            }, 50);
+            window.document.body.style.cursor = "wait";
+        };
+        /**
+         * Logs possible timeout
+         */
+        $scope.$on("possibleTimeout", function () {
+            $scope.SYNERGY.logger.error("Uknown response", "Seems like timeout occurred, please try to reload page", "DEBUG");
+        });
+
+        $scope.$on("hideUserMenu", function () {
+            $scope.SYNERGY.session.hideUserMenu();
+        });
+        $scope.$on("refreshRole", function () {
+            $scope.role = $scope.SYNERGY.session.role;
+        });
+
+        $scope.$on("busyMode", function (event, args) {
+            if (args) {
+                $scope.busyBrand = "brand_busy";
+            } else {
+                window.document.body.style.cursor = "default";
+                $scope.busyBrand = "";
+                ngProgress.complete(100);
+            }
+        });
+
+        /**
+         * Sends only check for user session, if there is none, discards any session information stored in browser
+         */
+        $scope.init = function (callback) {
+            var _c = $cookieStore.get("session"); //user key throws error
+            if ($scope.cookieChecked && _c && $scope.SYNERGY.session.cookieIsValid(_c.created) && typeof _c.token !== "undefined") {// && _c.length > 0
+                //var session = window.JSON.parse(_c);
+                var session = _c;
+                $scope.SYNERGY.session.hideLoginForm();
+                $scope.SYNERGY.session.showUserMenu(session.username);
+                $scope.SYNERGY.session.isLoggedIn = true;
+                $scope.SYNERGY.session.username = session.username;
+                $scope.SYNERGY.session.role = session.role;
+                $scope.SYNERGY.session.lastName = (session.hasOwnProperty("lastName") && session.lastName.length > 0) ? session.lastName : "";
+                $scope.SYNERGY.session.firstName = (session.hasOwnProperty("firstName") && session.firstName.length > 0) ? session.firstName : "";
+                $scope.role = session.role;
+                $scope.SYNERGY.session.session_id = session.session_id;
+                $scope.SYNERGY.session.created = session.created;
+                $scope.SYNERGY.session.token = session.session_id;
+                sessionService.setSession($scope.SYNERGY.session);
+                $timeout(callback, 0);
+            } else {
+                sessionHttp.infoConditional($scope, function (data) {
+                    $scope.cookieChecked = true;
+                    $scope.SYNERGY.session.hideLoginForm();
+                    $scope.SYNERGY.session.showUserMenu(data.username);
+                    $scope.SYNERGY.session.isLoggedIn = true;
+                    $scope.SYNERGY.session.username = data.username;
+                    $scope.SYNERGY.session.lastName = data.lastName;
+                    $scope.SYNERGY.session.firstName = data.firstName;
+                    $scope.SYNERGY.session.role = data.role;
+                    $scope.role = data.role;
+                    $scope.SYNERGY.session.session_id = data.session_id;
+                    $scope.SYNERGY.session.token = data.session_id;
+                    $scope.SYNERGY.session.created = data.created;
+                    sessionService.setSession($scope.SYNERGY.session);
+                    $cookieStore.put("session", ({firstName: data.firstName, lastName: data.lastName, username: data.username, role: data.role, token: data.token, created: 1000 * parseInt(data.created, 10), session_id: data.session_id}));
+                    callback();
+                }, function (data) {
+                    $cookieStore.remove("session");
+                    sessionService.clearSession();
+                    $scope.cookieChecked = false;
+                    //  $scope.SYNERGY.session.showLoginForm();
+                    $scope.SYNERGY.session.hideUserMenu();
+                    callback();
+                });
+            }
+        };
+
+        /**
+         * Redirects to login page if SSO is not used. If SSO is used, it sends "active" request to server. Server checks SSO session
+         * and if session exists and it's valid, returns user information. If session is not valid, server returns HTTP 307 and client redirects to 
+         * SSO login page.
+         */
+        $scope.login = function () {
+            if (!$scope.SYNERGY.useSSO) {
+                $location.path("login");
+            } else {
+                sessionHttp.get($scope, function (data) {
+                    $scope.SYNERGY.session.isLoggedIn = true;
+                    $scope.SYNERGY.session.username = data.username;
+                    $scope.SYNERGY.session.role = data.role;
+                    $scope.role = data.role;
+                    $scope.SYNERGY.session.lastName = data.lastName;
+                    $scope.SYNERGY.session.firstName = data.firstName;
+                    $scope.SYNERGY.session.created = 1000 * parseInt(data.created, 10);
+                    $scope.SYNERGY.session.session_id = data.session_id;
+                    $scope.SYNERGY.session.token = data.session_id;
+                    $scope.SYNERGY.session.hideLoginForm();
+                    $scope.SYNERGY.session.showUserMenu(data.username);
+                    sessionService.setSession($scope.SYNERGY.session);
+                    $cookieStore.put("session", ({username: data.username, role: data.role, token: data.token, created: 1000 * parseInt(data.created, 10), session_id: data.session_id}));
+                    window.location.reload();
+                }, function (data, status) {
+                    $scope.SYNERGY.logger.log("Action failed", data + ":" + status, "DEBUG", "alert-error");
+                    sessionService.clearSession();
+                    status = parseInt(status, 10);
+                    switch (status) {
+                        case 307:
+                            window.location.href = $scope.SYNERGY.getLoginRedirectUrl($scope.SYNERGY.ssoLoginUrl, window.location.href);
+                            break;
+                        case 400:
+                            $scope.SYNERGY.logger.log("Login failed", data, "INFO", "alert-error");
+                            break;
+                        default:
+                            $scope.SYNERGY.logger.log("Login failed", "Incorrect credentials, please try again ", "INFO", "alert-error");
+                            break;
+                    }
+                });
+            }
+        };
+
+        /**
+         * Sends logout request to serverpr
+         */
+        $scope.logout = function () {
+            $cookieStore.remove("session");
+            $scope.SYNERGY.session.clearSession();
+            $scope.role = "";
+            sessionService.clearSession();
+            sessionHttp.logout($scope, function (data) {
+                if ($scope.SYNERGY.useSSO) {
+                    window.location.href = $scope.SYNERGY.getLogoutRedirectUrl($scope.SYNERGY.ssoLogoutUrl, (window.location.href));
+                } else {
+                    window.location.reload();
+                }
+            }, function (data) {
+                $scope.SYNERGY.logger.log("Logout failed", data.toString(), "INFO", "alert-error");
+            });
+        };
+
+        /**
+         * Listens to updateNavbar and highlights received link in top nav bar
+         */
+        $scope.$on("updateNavbar", function (event, args) {
+            try {
+                $("ul#navbar li").each(function () {
+                    if ($(this).attr("class") === "active") {
+                        $(this).attr("class", "");
+                    }
+                });
+            } catch (e) {
+            }
+            try {
+                $("ul#navbar li#" + args.item).attr("class", "active");
+            } catch (e) {
+            }
+        });
+
+
+        function splitMergeBreadCrumbs(currentTitle, currentTitleIndex, breadCrumbs) {
+            var p1 = breadCrumbs.slice(0, currentTitleIndex);
+            var p2 = breadCrumbs.slice(currentTitleIndex + 1);
+            p2.push(currentTitle);
+            return p1.concat(p2);
+        }
+
+        /**
+         * Updates breadcrumbs menu
+         */
+        $scope.$on("updateBreadcrumbs", function (event, args) {
+            var i = 0;
+            if ($scope.breadcrumbs.length === 5) {
+                for (i = 0; i < 5; i++) {
+                    if (typeof $scope.breadcrumbs[i] !== "undefined" && $scope.breadcrumbs[i].title === args.title) {
+                        $scope.breadcrumbs = splitMergeBreadCrumbs(args, i, $scope.breadcrumbs);
+                        return;
+                    }
+                }
+
+                for (i = 0; i < 4; i++) {
+                    $scope.breadcrumbs[i] = $scope.breadcrumbs[i + 1];
+                }
+                $scope.breadcrumbs[4] = args;
+            } else {
+                for (i = 0; i < 4; i++) {
+                    if (typeof $scope.breadcrumbs[i] !== "undefined" && $scope.breadcrumbs[i].title === args.title) {
+                        $scope.breadcrumbs = splitMergeBreadCrumbs(args, i, $scope.breadcrumbs);
+                        return;
+                    }
+                }
+
+                if (typeof $scope.breadcrumbs[$scope.breadcrumbs.length] === "undefined" && (typeof $scope.breadcrumbs[$scope.breadcrumbs.length - 1] === "undefined" || $scope.breadcrumbs[$scope.breadcrumbs.length - 1].title !== args.title)) {
+                    $scope.breadcrumbs[$scope.breadcrumbs.length] = args;
+                }
+            }
+        });
+
+        /**
+         * Listens on key press (Enter) when cursor is in search field
+         */
+        $scope.synergySearch = function () {
+            $location.path("search/" + ($scope.searchedItem));
+        };
+
+        $scope.goToSearch = function (suggestedItem) {
+            $location.path(suggestedItem.link);
+        };
+
+        $scope.searchAhead = function () {
+            if ($scope.searchedItem.length < 2) {
+                return;
+            }
+            searchHttp.getFewSpecifications($scope, $scope.searchedItem, function (data) {
+                var a = [];
+                for (var i = 0, max = data.length; i < max; i++) {
+                    if (data[i].project === null || data[i].project.length < 1) {
+                        a.push({title: data[i].title + " (" + $scope.SYNERGY.product + " " + data[i].version + ")", link: data[i].type + "/" + data[i].id});
+                    } else {
+                        a.push({title: data[i].title + " (" + data[i].project + " " + data[i].version + ")", link: data[i].type + "/" + data[i].id});
+                    }
+
+                }
+                $scope.suggestions = a;
+                //   $("#typeahead").typeahead({source: a});
+            }, function (data) {
+                $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+            });
+        };
+
+        $scope.encodeURIComponent = function (s) {
+            return encodeURIComponent(s);
+        };
+
+        // register bugtracking link scripts for all projects on page load  
+        projectsHttp.getAll($scope, function (data) { // todo add caching
+            for (var i = 0, max = data.length; i < max; i++) {
+                if (data[i].reportLink !== null || data[i].viewLink !== null) {
+                    var fnca,
+                            fncb,
+                            fncc;
+                    /* jshint ignore:start */
+                    try {
+                        fnca = eval("(function(){ var a=" + data[i].viewLink + "; return a;})(); ");
+                    } catch (e) {
+                        fnca = null;
+                    }
+                    try {
+                        fncb = eval("(function(){ var a=" + data[i].multiViewLink + "; return a;})(); ");
+                    } catch (e) {
+                        fncb = null;
+                    }
+                    try {
+                        fncc = eval("(function(){ var a=" + data[i].reportLink + "; return a;})(); ");
+                    } catch (e) {
+                        fncc = null;
+                    }
+
+                    $scope.SYNERGY.bugtrackingSystems[data[i].name] = {
+                        getDisplayLink: fnca,
+                        getMultiDisplayLink: fncb,
+                        getReportLink: fncc
+                    };
+                    /* jshint ignore:end */
+                }
+            }
+        }, function () {
+        });
+
+    }
+    function SearchCtrl($scope, $routeParams, searchHttp) {
+
+        $scope.searched = $routeParams.search;
+        $scope.results = [];
+        $scope.escapedSearched = decodeURIComponent($scope.searched);
+
+        $scope.fetch = function () {
+            searchHttp.get($scope, $scope.searched, function (data) {
+                $scope.results = data;
+            }, function (data) {
+                $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+            });
+        };
+
+        $scope.printResult = function (item) {
+            switch (item.type) {
+                case "specification":
+                    return "<a href=\"#specification/" + item.id + "\">" + item.title + " (" + $scope.SYNERGY.product + " " + item.version + ")" + "</a>";
+                case "suite":
+                    return "<a href=\"#suite/" + item.id + "\">" + item.title + "</a>";
+                default:
+                    break;
+            }
+        };
+
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+    /**
+     * Loads list of test specifications
+     * @param {SpecificationsFct} specificationsHttp description
+     * @param {VersionsFct} versionsHttp description
+     * @param {UserFct} userFct description
+     * @param {SpecificationFct} specificationFct description
+     */
+    function SpecPoolCtrl($scope, $routeParams, specificationsHttp, versionsHttp) {//authService
+        $scope.$emit("updateNavbar", {item: "nav_specs"});
+        $scope.specs = [];
+        $scope.version = $routeParams.id || null; // selected version
+        $scope.versions = []; // all versions available
+        $scope.projects = [];
+        $scope.orderProp = "title";
+        $scope.rights = 0;
+        $scope.isLoggedIn = (typeof $scope.SYNERGY.session.session_id !== "undefined" && $scope.SYNERGY.session.session_id.length > 1) ? 1 : 0;
+        /**
+         * used for caching specifications in given version. This in-memory cache is valid only while user in on this page
+         */
+        var cache = {};
+
+        /**
+         * Loads data from server
+         */
+        function init() {
+            $scope.isLoggedIn = (typeof $scope.SYNERGY.session.session_id !== "undefined" && $scope.SYNERGY.session.session_id.length > 1) ? 1 : 0;
+            if (typeof $scope.SYNERGY.session.session_id !== "undefined" && $scope.SYNERGY.session.session_id.length > 1) {
+                $scope.rights = 1;
+            }
+            versionsHttp.get($scope, true, function (data) {
+                data.unshift({id: -1, name: "all"});
+                $scope.versions = data;
+                $scope.version = $scope.version || data[0].name;
+                specificationsHttp.get($scope, $scope.version, function (data) {
+
+
+                    var _projects = [];
+                    for (var i = 0, imax = data.length; i < imax; i++) {
+                        if (data[i].projects.length > 0) {
+                            data[i]._project = data[i].projects[0].name;
+                            if (_projects.indexOf(data[i].projects[0].name) < 0) {
+                                _projects.push(data[i].projects[0].name);
+                            }
+                        } else {
+                            data[i]._project = $scope.SYNERGY.product;
+                            data[i].projects = [{name: $scope.SYNERGY.product, id: -2}];
+                            if (_projects.indexOf($scope.SYNERGY.product) < 0) {
+                                _projects.push($scope.SYNERGY.product);
+                            }
+                        }
+                    }
+                    _projects.push("All");
+                    $scope.projects = _projects;
+                    cache[$scope.version + "projects"] = _projects;
+
+                    $scope.specs = data;
+                    cache[$scope.version] = data;
+                    $scope.$emit("updateBreadcrumbs", {link: "specifications", title: "Test Specifications"});
+
+                }, function (data, status) {
+                    if (parseInt(status, 10) !== 404) {
+                        $scope.SYNERGY.logger.log("Action failed", data.toString(), "INFO", "alert-error");
+                    } else {
+                        $scope.SYNERGY.logger.log("", "No results", "INFO");
+                    }
+                });
+            }, $scope.generalHttpFactoryError);
+        }
+
+        $scope.init(function () {
+            init();
+        });
+
+        /**
+         * Loads specifications for given version. First it checks cache and if it doesn't contain data for
+         * given version, asks server
+         */
+        $scope.filter = function () {
+
+            if (cache.hasOwnProperty($scope.version)) {
+                $scope.specs = cache[$scope.version];
+                $scope.projects = cache[$scope.version + "projects"];
+                return;
+            }
+
+            specificationsHttp.get($scope, $scope.version, function (data) {
+                var _projects = [];
+                for (var i = 0, imax = data.length; i < imax; i++) {
+                    if (data[i].ext.hasOwnProperty("projects") && data[i].ext.projects.length > 0) {
+                        data[i]._project = data[i].ext.projects[0].name;
+                        if (_projects.indexOf(data[i].ext.projects[0].name) < 0) {
+                            _projects.push(data[i].ext.projects[0].name);
+                        }
+                    } else {
+                        data[i]._project = $scope.SYNERGY.product;
+                        if (_projects.indexOf($scope.SYNERGY.product) < 0) {
+                            _projects.push($scope.SYNERGY.product);
+                        }
+                    }
+                }
+                _projects.push("All");
+                $scope.projects = _projects;
+                $scope.specs = data;
+                cache[$scope.version] = data;
+                cache[$scope.version + "projects"] = _projects;
+                $scope.$emit("updateBreadcrumbs", {link: "specifications", title: "Test Specifications"});
+
+            }, function (data, status) {
+                if (parseInt(status, 10) !== 404) {
+                    $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+                } else {
+                    $scope.SYNERGY.logger.log("", "No results", "INFO");
+                }
+            });
+        };
+
+    }
+    /**
+     *  
+     * @param {SpecificationsFct} specificationsHttp
+     * @param {RunsFct} runsHttp
+     * @returns {undefined} 
+     **/
+    function HomeCtrl($scope, specificationsHttp, runsHttp, calendarHttp) {
+
+        $scope.runs = {testRuns: []};
+        $scope.specs = [];
+        $scope.$emit("updateNavbar", {item: "nav_home"});
+
+        /**
+         * Loads latest test runs and test specifications
+         */
+        $scope.fetch = function () {
+            runsHttp.getLatest($scope, 7, function (data) {
+                if (typeof data.testRuns !== "undefined") {
+
+                    data.testRuns.forEach(function (trun) {
+                        if (trun.projectName === null || trun.projectName === "") {
+                            trun.projectName = $scope.SYNERGY.product;
+                        }
+                    });
+
+                    $scope.runs = data;
+                }
+            }, function (data) {
+                $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+            });
+            specificationsHttp.latest($scope, function (result) {
+                $scope.specs = result;
+                $scope.SYNERGY.publisher.publish(1, $scope, "specListLoaded");
+            }, function (data) {
+                $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+            });
+        };
+
+        function loadCalendar() {
+            calendarHttp.getEvents($scope, function (data) {
+                var items = [];
+
+                for (var i = 0, max = data.length; i < max; i += 1) {
+                    items.push({url: "#/run/" + data[i].id, title: data[i].title, start: new Date(data[i].start.substr(0, 4), parseInt(data[i].start.substr(4, 2), 10) - 1, data[i].start.substr(6, 2)), end: new Date(data[i].end.substr(0, 4), parseInt(data[i].end.substr(4, 2), 10) - 1, data[i].end.substr(6, 2))});
+                }
+
+                $("#cal").fullCalendar({
+                    header: {
+                        left: "prev,next today",
+                        center: "title",
+                        right: "month,agendaWeek,agendaDay"
+                    },
+                    editable: true,
+                    events: items,
+                    eventMouseover: function (event, jsEvent, view) {
+                        if (view.name !== "agendaDay") {
+                            $(jsEvent.target).attr("title", event.title);
+                        }
+                    }
+                });
+            }, function () {
+                window.console.log("Failed to load calendar events");
+            });
+        }
+
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+            loadCalendar();
+        });
+    }
+    /**
+     * View 1
+     * @param {RunFct} runHttp
+     * @param {AssignmentFct} assignmentHttp description
+     * @param {AttachmentFct} attachmentHttp description
+     * @returns {undefined}
+     */
+    function RunCtrl($scope, utils, $location, $routeParams, runHttp, assignmentHttp, attachmentHttp, reviewHttp, SynergyUtils, SynergyHandlers, SynergyIssue) {
+
+        $scope.$emit("updateNavbar", {item: "nav_runs"});
+        $scope.$emit("updateBreadcrumbs", {link: "runs", title: "Test Runs"});
+        $scope.id = $routeParams.id || -1;
+        $scope.run = {};
+        $scope.rights = 0;
+        var currentActionId = -1;
+        var currentAction = "";
+        $scope.username = $scope.SYNERGY.session.username || "";
+        $scope.attachmentBase = $scope.SYNERGY.server.buildURL("run_attachment", {});
+        $scope.isLoading = false;
+        $scope.tribes = [];
+        var tribes = [];
+        var leaderIsRemoving = false;
+        $scope.explainModal = "";
+        $scope.filter = {
+            "assignee": "All",
+            "specification": "All",
+            "platform": "All",
+            "tribe": "All"
+        };
+        var issueCollector = new SynergyIssue.RunIssuesCollector();
+        $scope.P1Issues = [];
+        $scope.P2Issues = [];
+        $scope.P3Issues = [];
+        $scope.unresolvedIssues = [];
+        $scope.allIssues = [];
+        $scope.specifications = [];
+        $scope.assignees = [];
+        $scope.project = {"name": "", "id": -1};
+        $scope.pageSize = $scope.SYNERGY.assignmentPage;
+        var _coverage = {};
+        $scope.coverage = {};
+        $scope.sortConfig = {
+            property: ["id"],
+            descending: [false]
+        };
+        $scope.orderingProperties = {
+            "userDisplayName": {
+                "desc": false,
+                "asc": false
+            },
+            "specification": {
+                "desc": false,
+                "asc": false
+            },
+            "platform": {
+                "desc": false,
+                "asc": false
+            }
+        };
+        $scope.testRunIsAvailable = false;
+        $scope.pageReviewsExpanded = false;
+
+        /**
+         * Loads test run
+         */
+        $scope.fetch = function () {
+            tribes = [];
+            $scope.run = {};
+            issueCollector = new SynergyIssue.RunIssuesCollector();
+            $scope.P1Issues = [];
+            $scope.P2Issues = [];
+            $scope.P3Issues = [];
+            $scope.unresolvedIssues = [];
+            $scope.allIssues = [];
+            $scope.specifications = [];
+            $scope.assignees = [];
+            $scope.project = {"name": "", "id": -1};
+            _coverage = {};
+            $scope.coverage = {};
+            $scope.username = $scope.SYNERGY.session.username || "";
+            runHttp.get($scope, $scope.id, function (data) {
+                setProject(data);
+                countResults(data);
+                setTestRunIsAvailable(data);
+                $scope.run = data;
+                collectFilterData(data);
+                $scope.$emit("updateBreadcrumbs", {link: "run/" + $scope.id + "/v/1", title: $scope.run.title});
+                SynergyUtils.ProgressChart([((data.completed / data.total) * 100), 100 - ((data.completed / data.total) * 100)], ["#08c", "#ccc"], ["completed", ""], "canvas2");
+                try {
+                    if (data.controls.length > 0) {
+                        $scope.rights = 1;
+                    }
+                } catch (e) {
+                }
+            }, $scope.generalHttpFactoryError);
+        };
+
+        function setTestRunIsAvailable(testRun) {
+            var start = $scope.getDate(testRun.start);
+            var stop = $scope.getDate(testRun.end);
+            var today = new Date();
+            $scope.testRunIsAvailable = (today >= start && today <= stop);
+        }
+
+        function setProject(data) {
+            if (data.projectName !== null) {
+                $scope.project = {"name": data.projectName, "id": data.projectId};
+            } else {
+                $scope.project = {"name": $scope.SYNERGY.product, "id": -2};
+            }
+        }
+
+        $scope.toggleReivewSection = function () {
+            $scope.pageReviewsExpanded = !$scope.pageReviewsExpanded;
+            $("#pageReviews").collapse("toggle");
+        };
+
+        function collectFilterData(data) {
+            var assignees = [];
+            var platforms = [];
+            var specifications = [];
+            var tribes = [];
+            for (var i = 0, max = data.assignments.length; i < max; i++) {
+                if (assignees.indexOf(data.assignments[i].userDisplayName) < 0) {
+                    assignees.push(data.assignments[i].userDisplayName);
+                }
+                if (specifications.indexOf(data.assignments[i].specification) < 0) {
+                    specifications.push(data.assignments[i].specification);
+                }
+                if (platforms.indexOf(data.assignments[i].platform) < 0) {
+                    platforms.push(data.assignments[i].platform);
+                }
+                for (var j = 0, max2 = data.assignments[i].tribes.length; j < max2; j++) {
+                    if (tribes.indexOf(data.assignments[i].tribes[j]) < 0) {
+                        tribes.push(data.assignments[i].tribes[j]);
+                    }
+                }
+            }
+            assignees.push("All");
+            assignees.sort(function (a, b) {
+                return a.toLowerCase() < b.toLowerCase() ? -1 : 1;
+            });
+
+            specifications.push("All");
+            specifications.sort(function (a, b) {
+                return a.toLowerCase() < b.toLowerCase() ? -1 : 1;
+            });
+            tribes.push("All");
+            tribes.sort(function (a, b) {
+                return a.toLowerCase() < b.toLowerCase() ? -1 : 1;
+            });
+
+            platforms.push("All");
+            platforms.sort(function (a, b) {
+                return a.toLowerCase() < b.toLowerCase() ? -1 : 1;
+            });
+
+            $scope.assignees = assignees;
+            $scope.platforms = platforms;
+            $scope.specifications = specifications;
+            $scope.tribes = tribes;
+        }
+
+        $scope.assignessFilter = function (assignmentRecord) {
+            if ($scope.filter.assignee && $scope.filter.assignee !== "All" && assignmentRecord.userDisplayName !== $scope.filter.assignee) {
+                return false;
+            }
+            if ($scope.filter.specification && $scope.filter.specification !== "All" && assignmentRecord.specification !== $scope.filter.specification) {
+                return false;
+            }
+            if ($scope.filter.platform && $scope.filter.platform !== "All" && assignmentRecord.platform !== $scope.filter.platform) {
+                return false;
+            }
+            if ($scope.filter.tribe && $scope.filter.tribe !== "All" && assignmentRecord.tribes.indexOf($scope.filter.tribe) < 0) {
+                return false;
+            }
+            return true;
+        };
+
+        /**
+         * Counts number of passed/failed/skipped cases
+         * @param {TestRun} run
+         */
+        function countResults(run) {
+            var result = {
+                "failed": 0,
+                "passed": 0,
+                "skipped": 0
+            };
+
+            if (SynergyUtils.definedNotNull(run) && SynergyUtils.definedNotNull(run.assignments)) {
+                for (var i = 0, max = run.assignments.length; i < max; i++) {
+                    if (!_coverage[run.assignments[i].platform]) {// FIXME
+                        _coverage[run.assignments[i].platform] = {total: 0, completed: 0, name: run.assignments[i].platform};
+                    }
+                    issueCollector.addIssues(run.assignments[i].issues);
+                    _coverage[run.assignments[i].platform].total += parseInt(run.assignments[i].total, 10);
+                    _coverage[run.assignments[i].platform].completed += parseInt(run.assignments[i].completed, 10);
+                    result.failed += Math.floor(run.assignments[i].failed);
+                    result.passed += Math.floor(run.assignments[i].passed);
+                    result.skipped += Math.floor(run.assignments[i].skipped);
+                }
+
+                var t = (result.failed + result.passed + result.skipped) / 100;
+                if (t > 0) {
+                    var f = Math.floor(result.failed * 10 / t) / 10;
+                    var p = Math.round(result.passed * 10 / t) / 10;
+                    SynergyUtils.ProgressChart([p, f, Math.round(10 * (100 - (f + p))) / 10], ["#62c462", "#ee5f5b", "#c67605"], ["passed", "failed", "skipped"], "canvas1");
+                }
+            }
+
+            for (var j in _coverage) {
+                if (_coverage.hasOwnProperty(j)) {
+                    _coverage[j].progress = Math.round(10 * 100 * (_coverage[j].completed / _coverage[j].total)) / 10;
+                }
+            }
+            $scope.coverage = _coverage;
+            SynergyUtils.ProgressChart([issueCollector.issuesStats.opened / (issueCollector.issuesStats.total / 100), 100 - (issueCollector.issuesStats.opened / (issueCollector.issuesStats.total / 100))], ["#ccc", "#62c462"], ["Unresolved", "Resolved"], "issuesResolution");
+            SynergyUtils.ProgressChart([issueCollector.issuesStats.P1 / (issueCollector.issuesStats.total / 100), issueCollector.issuesStats.P2 / (issueCollector.issuesStats.total / 100), issueCollector.issuesStats.P3 / (issueCollector.issuesStats.total / 100), issueCollector.issuesStats.P4 / (issueCollector.issuesStats.total / 100)], ["#ee5f5b", "#f89406", "#fbeed5", "#ccc"], ["P1 (" + issueCollector.issuesStats.P1 + ")", "P2 (" + issueCollector.issuesStats.P2 + ")", "P3 (" + issueColle [...]
+            $scope.allIssues = issueCollector.issues;
+            $scope.unresolvedIssues = issueCollector.issuesStats.unresolvedIssues;
+            $scope.P1Issues = issueCollector.issuesStats.P1Issues;
+            $scope.P2Issues = issueCollector.issuesStats.P2Issues;
+            $scope.P3Issues = issueCollector.issuesStats.P3Issues;
+
+        }
+        $scope.createCoverageChart = function (c) {
+            try {
+                SynergyUtils.ProgressChart([c.progress, 100 - c.progress], ["#08c", "#ccc"], ["finished", ""], "coverage" + c.name);
+            } catch (e) {
+            }
+        };
+
+        $scope.nextPage = function () {
+            $scope.isLoading = true;
+            $scope.pageSize += $scope.SYNERGY.assignmentPage;
+            $scope.isLoading = false;
+        };
+
+        function resetFilters() {
+            $scope.sortConfig = {
+                "property": ["id"],
+                "descending": [false]
+            };
+            $scope.orderingProperties = {
+                "userDisplayName": {
+                    "desc": false,
+                    "asc": false
+                },
+                "specification": {
+                    "desc": false,
+                    "asc": false
+                },
+                "platform": {
+                    "desc": false,
+                    "asc": false
+                }
+            };
+        }
+
+        $scope.changeSorting = function (prop, order) {
+            var _p = prop;
+            prop = order ? prop : "-" + prop;
+            if ($scope.sortConfig.property.indexOf(_p) > -1 || $scope.sortConfig.property.indexOf("-" + _p) > -1) {
+                var i = $scope.sortConfig.property.indexOf(_p);
+                if (i < 0) {
+                    i = $scope.sortConfig.property.indexOf("-" + _p);
+                }
+                if ($scope.sortConfig.descending[i] === order) { // click on the same order arrow => remove it from filter
+                    $scope.sortConfig.property.splice(i, 1);
+                    $scope.sortConfig.descending.splice(i, 1);
+                    $scope.orderingProperties[_p].desc = false; // reset css for this property
+                    $scope.orderingProperties[_p].asc = false;
+                    if ($scope.sortConfig.property.length === 0) { // resel css to default values and reset filter to ID 
+                        resetFilters();
+                    }
+                } else {
+                    $scope.orderingProperties[_p].desc = !$scope.orderingProperties[_p].desc; // invert css
+                    $scope.orderingProperties[_p].asc = !$scope.orderingProperties[_p].asc;
+                    var _orig = {// just place holder
+                        "descending": !$scope.sortConfig.descending[i],
+                        "property": $scope.sortConfig.property[i]
+                    };
+                    $scope.sortConfig.descending.splice(i, 1); // remove it from array of filters
+                    $scope.sortConfig.property.splice(i, 1);
+                    $scope.sortConfig.descending.splice(0, 0, _orig.descending); // insert it to level of filters at the beginning
+                    (_orig.property.indexOf("-") === 0) ? $scope.sortConfig.property.splice(0, 0, _orig.property.substring(1, _orig.property.length)) : $scope.sortConfig.property.splice(0, 0, "-" + _orig.property);
+                }
+            } else {
+                if ($scope.sortConfig.property.length === 1 && $scope.sortConfig.property[0] === "id") { // if no ordering so far, simply replace ID with selected property
+                    $scope.sortConfig = {
+                        property: [prop],
+                        descending: [order]
+                    };
+                } else {
+                    $scope.sortConfig.property.splice(0, 0, prop); // insert at the beginning
+                    $scope.sortConfig.descending.splice(0, 0, order);
+                }
+
+                $scope.orderingProperties[_p].desc = !order; // css update
+                $scope.orderingProperties[_p].asc = order;
+            }
+        };
+
+        $scope.bugs = [];
+        $scope.bugsAssignmentId = -1;
+
+        $scope.alterBugs = function (assignment) {
+            $scope.bugsAssignmentId = assignment.id;
+            var a = [];
+            for (var i = 0, max = assignment.issues.length; i < max; i++) {
+                a.push({
+                    "id": assignment.issues[i].bugId,
+                    "stillValid": true,
+                    "changeCount": false
+                });
+            }
+            $scope.bugs = a;
+            $("#ticketsModal").modal("toggle");
+        };
+
+        $scope.performAlterBugs = function () {
+            var newIssues = [];
+            var countDiff = 0;
+            for (var i = 0, max = $scope.bugs.length; i < max; i++) {
+                if ($scope.bugs[i].stillValid) {
+                    newIssues.push($scope.bugs[i].id);
+                } else {
+                    if ($scope.bugs[i].changeCount) {
+                        countDiff++;
+                    }
+                }
+            }
+
+            assignmentHttp.alterBugs($scope, $scope.bugsAssignmentId, {issues: newIssues.join(";"), diffCount: countDiff}, function () {
+                $scope.SYNERGY.logger.log("Done", "Issues updated", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+
+
+            $("#ticketsModal").modal("toggle");
+        };
+        /**
+         * Redirects to page so user starts testing and completing his assignments
+         * @param {Number} mode
+         * @param {Number} assignmentId assignment ID
+         */
+        $scope.startAssignment = function (mode, assignmentId) {
+            if (parseInt(mode, 10) === 2) {// restart => show modal confirmation
+                $("#deleteModalLabel").text("Restart assignment?");
+                $("#deleteModalBody").html("<p>Do you really want to restart this assignment? All saved progress will be lost as if you never started it. If you want to Continue saved assignment, please use 'Play' button instead</p>");
+                $("#deleteModal").modal("toggle");
+                currentAction = "restartAssignment";
+                currentActionId = assignmentId;
+            } else {
+                $location.path("/assignment/" + assignmentId + "/v/" + mode);
+            }
+        };
+
+        $scope.startReviewAssignment = function (mode, assignmentId) {
+            if (parseInt(mode, 10) === 2) {// restart => show modal confirmation
+                $("#deleteModalLabel").text("Restart assignment?");
+                $("#deleteModalBody").html("<p>Do you really want to restart this assignment? All saved comments will be lost as if you never started it. If you want to Continue saved assignment, please use 'Play' button instead</p>");
+                $("#deleteModal").modal("toggle");
+                currentAction = "restartReviewAssignment";
+                currentActionId = assignmentId;
+            } else {
+                $location.path("/review/" + assignmentId + "/continue");
+            }
+        };
+
+        /**
+         * Starts with action on given test run. If the action name is different than "delete", redirection is done.
+         * Otherwise confirmation dialog is opened
+         * @param {String} action action name
+         */
+        $scope.performRun = function (action) {
+            switch (action) {
+                case "delete":
+                    $("#deleteModalLabel").text("Delete test run?");
+                    $("#deleteModalBody").html("<p>Do you really want to delete test run?</p>");
+                    $("#deleteModal").modal("toggle");
+                    currentAction = "deleteRun";
+                    break;
+                case "notify":
+                    $("#deleteModalLabel").text("Send notifications?");
+                    $("#deleteModalBody").html("<p>Do you really want to send email notifications to testers with incomplete test assignment?</p>");
+                    $("#deleteModal").modal("toggle");
+                    currentAction = "notify";
+                    break;
+                case "freeze":
+                    var target = ($scope.run.isActive ? 0 : 1);
+                    runHttp.freezeRun($scope, $scope.id, target, function (data) {
+                        $scope.run.isActive = target;
+                        $scope.SYNERGY.logger.log("Done", "Test run " + (target === 1 ? "unfrozen" : "frozen"), "INFO", "alert-success");
+                    }, $scope.generalHttpFactoryError);
+                    break;
+                default:
+                    $location.path("/administration/run/" + $scope.id + "/" + action);
+                    break;
+            }
+        };
+
+        /**
+         * Starts with action on given test assignment
+         * Otherwise confirmation dialog is opened
+         * @param {String} action action name
+         * @param {Number} id assignment ID
+         */
+        $scope.performAssignment = function (action, id, createdBy) {
+            if (action !== "delete") {
+                $location.path("suite/" + id + "/" + action);
+            } else {
+                switch (action) {
+                    case "delete":
+                        currentAction = "deleteAssignment";
+                        currentActionId = id;
+                        leaderIsRemoving = (createdBy === 3) ? true : false;
+                        $("#deleteModalLabel").text("Delete test assignment?");
+                        $("#deleteModalBody").html("<p>Do you really want to delete test assignment?</p>");
+                        $("#deleteModal").modal("toggle");
+                        break;
+                    default:
+                        break;
+                }
+            }
+        };
+        $scope.performReviewAssignment = function (action, id, createdBy) {
+            switch (action) {
+                case "delete":
+                    currentAction = "deleteReviewAssignment";
+                    currentActionId = id;
+                    leaderIsRemoving = (createdBy === 3) ? true : false;
+                    $("#deleteModalLabel").text("Delete review assignment?");
+                    $("#deleteModalBody").html("<p>Do you really want to delete review assignment?</p>");
+                    $("#deleteModal").modal("toggle");
+                    break;
+                default:
+                    $location.path("/review/" + id + "/" + action);
+                    break;
+            }
+        };
+
+        function deleteReviewAssignment() {
+            if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                return;
+            }
+            reviewHttp.remove($scope, currentActionId, function (data) {
+                $scope.SYNERGY.logger.log("Done", "Assignment deleted", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+        }
+
+        $scope.deleteAssignment = function () {
+            if (!leaderIsRemoving) {
+
+                if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                    return;
+                }
+                assignmentHttp.remove($scope, currentActionId, function (data) {
+                    $scope.SYNERGY.logger.log("Done", "Assignment deleted", "INFO", "alert-success");
+                    $scope.fetch();
+                }, $scope.generalHttpFactoryError);
+            } else {
+                $("#explainModal").modal("toggle");
+                if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1 || $scope.explanation.length < 1) {
+                    return;
+                }
+                assignmentHttp.removeByLeader($scope, currentActionId, $scope.explanation, function (data) {
+                    $scope.SYNERGY.logger.log("Done", "Assignment deleted", "INFO", "alert-success");
+                    $scope.fetch();
+                }, $scope.generalHttpFactoryError);
+            }
+        };
+
+        /**
+         * Executes some action based on value of $scope.currentAction
+         */
+        $scope.performAction = function () {
+            switch (currentAction) {
+                case "restartAssignment":
+                    $("#deleteModal").modal("toggle");
+                    $location.path("/assignment/" + currentActionId + "/v/2");
+                    break;
+                case "restartReviewAssignment":
+                    $("#deleteModal").modal("toggle");
+                    $location.path("/review/" + currentActionId + "/restart");
+                    break;
+                case "deleteAssignment":
+                    $("#deleteModal").modal("toggle");
+                    leaderIsRemoving ? $("#explainModal").modal("toggle") : $scope.deleteAssignment();
+                    break;
+                case "deleteReviewAssignment":
+                    $("#deleteModal").modal("toggle");
+                    deleteReviewAssignment();
+                    break;
+                case "notify":
+                    $("#deleteModal").modal("toggle");
+                    runHttp.sendNotifications($scope, $scope.id, function (data) {
+                        $scope.SYNERGY.logger.log("Done", data, "INFO", "alert-success");
+                    }, function (data) {
+                        $scope.SYNERGY.logger.log("Action failed", "", "INFO", "alert-error");
+                        $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+                    });
+                    break;
+                case "deleteRun":
+                    $("#deleteModal").modal("toggle");
+                    if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                        return;
+                    }
+                    runHttp.remove($scope, $scope.id, function (data) {
+                        $scope.SYNERGY.modal.update("Test run removed", "");
+                        $scope.SYNERGY.modal.show();
+                        $location.path("/runs");
+                    }, function (data) {
+                        $scope.SYNERGY.modal.update("Action failed", "");
+                        $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+                        $scope.SYNERGY.modal.show();
+                    });
+                    break;
+                case "deleteAttachment":
+                    $("#deleteModal").modal("toggle");
+                    if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                        return;
+                    }
+                    attachmentHttp.removeRunAttachment($scope, currentActionId, function (data) {
+                        $scope.SYNERGY.logger.log("Done", "Attachment deleted", "INFO", "alert-success");
+                        $scope.fetch();
+                    }, function (data) {
+                        $scope.SYNERGY.logger.log("Action failed", "", "INFO", "alert-error");
+                        $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+                        $scope.fetch();
+                    });
+                    break;
+                default:
+                    break;
+            }
+        };
+
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+
+        // ATTACHMENT UPLOAD HANDLING
+        new SynergyHandlers.FileUploader([], "dropbox", $scope.SYNERGY.uploadFileLimit, $scope.SYNERGY.server.buildURL("run_attachment", {"id": $scope.id}), function (title, msg, level, style, fileName) {
+            $scope.SYNERGY.logger.log(title, msg, level, style);
+            $scope.fileName = fileName;
+            $scope.fetch();
+        }, function (title, msg, level, style) {
+            $scope.SYNERGY.logger.log(title, msg, level, style);
+        });
+    }
+
+
+
+    function RunCtrlCase($scope, utils, $location, $routeParams, runHttp, SynergyUtils, SynergyHandlers, SynergyIssue) {
+        $scope.$emit("updateNavbar", {item: "nav_runs"});
+        $scope.id = $routeParams.id || -1;
+
+        $scope.run = {};
+        $scope.project = {"name": "", "id": -1};
+        $scope.isLoading = false;
+        $scope.testRunIsAvailable = false;
+        $scope.specs = [];
+        $scope.overview = {
+            totalTc: 0,
+            pRate: 0,
+            testers: 0,
+            duration: "",
+            time: ""
+        };
+
+        $scope.labels = [];
+        $scope.selectedLabels = [];
+        $scope.resultFilter = {
+            passed: true,
+            failed: true,
+            skipped: true,
+            passed_with_issues: true
+        };
+
+        var allExpanded = false;
+
+        function setProject(data) {
+            if (data.projectName !== null) {
+                $scope.project = {"name": data.projectName, "id": data.projectId};
+            } else {
+                $scope.project = {"name": $scope.SYNERGY.product, "id": -2};
+            }
+        }
+
+        /**
+         * Loads test run
+         */
+        $scope.fetch = function () {
+            $scope.project = {"name": "", "id": -1};
+            $scope.isLoading = true;
+            $scope.run = {};
+            runHttp.getBlobs($scope, $scope.id, function (data) {
+                $scope.run = data;
+                setProject(data);
+                $scope.$emit("updateBreadcrumbs", {link: "run/" + $scope.id + "/v/3", title: $scope.run.title});
+                $scope.isLoading = false;
+                buildData();
+            }, $scope.generalHttpFactoryError);
+        };
+
+        $scope.toggleExpand = function (item) {
+            item.expanded = !item.expanded;
+            if (item.suites) {
+                $("#spec" + item.id).collapse("toggle");
+            } else {
+                $("#suite" + item.id).collapse("toggle");
+            }
+        };
+
+        $scope.toggleAll = function () {
+            if (allExpanded) {
+                $(".collapse").collapse("hide");
+            } else {
+                $(".collapse").collapse("show");
+            }
+            allExpanded = !allExpanded;
+        };
+
+        function buildData() {
+            var arrObj = {};
+            var knownSpecs = [];
+            var _currentSuite;
+            var _currentSpec;
+
+            var totalTc = 0;
+            var passedTc = 0;
+            var failedTc = 0;
+            var skippedTc = 0;
+            var totalMins = 0;
+
+            $scope.run.durations.forEach(function (x) {
+                totalMins += x.duration;
+            });
+
+            var passedWithIssues = 0;
+            var _users = [];
+            var _allLabels = [];
+
+            $scope.run.blobs.forEach(function (blob) {
+                if (_users.indexOf(blob.user) < 0) {
+                    _users.push(blob.user);
+                }
+                if (blob.label.length > 0 && _allLabels.indexOf(blob.label) < 0) {
+                    _allLabels.push(blob.label);
+                }
+
+                if (knownSpecs.indexOf(blob.specification.id) < 0) {
+                    knownSpecs.push(blob.specification.id);
+                    arrObj["_" + blob.specification.id] = {
+                        name: blob.specification.name,
+                        id: blob.specification.id,
+                        expanded: false,
+                        label: blob.label,
+                        version: blob.specification.version,
+                        suites: {}
+                    };
+                }
+                _currentSpec = arrObj["_" + blob.specification.id];
+                // for each suite
+                blob.specification.suites.forEach(function (suite) {
+
+                    if (!_currentSpec.suites.hasOwnProperty("_" + suite.id)) {
+                        _currentSpec.suites["_" + suite.id] = {
+                            name: suite.name,
+                            id: suite.id,
+                            expanded: false,
+                            testCases: {}
+                        };
+                    }
+
+                    _currentSuite = _currentSpec.suites["_" + suite.id];
+
+                    suite.testCases.forEach(function (tc) {
+                        if (!_currentSuite.testCases.hasOwnProperty("_" + tc.id)) {
+                            _currentSuite.testCases["_" + tc.id] = {
+                                name: tc.name,
+                                id: tc.id,
+                                results: []
+                            };
+                        }
+                        if (tc.finished === 1) {
+                            totalTc++;
+                            var _s = getResult(tc);
+                            if (_s === "passed") {
+                                passedTc++;
+                            } else if (_s === "passed with issues") {
+                                passedTc++;
+                                passedWithIssues++;
+                            } else if (_s === "failed") {
+                                failedTc++;
+                            } else if (_s === "skipped") {
+                                skippedTc++;
+                            }
+                            _currentSuite.testCases["_" + tc.id].results.push({
+                                result: _s,
+                                visible: true,
+                                issuesLbl: tc.issues.length > 1 ? "issues" : (tc.issues.length === 0 ? "" : tc.issues[0]),
+                                link: "#/case/" + tc.id + "/suite/" + suite.id,
+                                resultClass: _s.replace(/\s/g, "_"),
+                                platform: blob.platform,
+                                user: blob.user,
+                                issues: tc.issues.length > 0 ? tc.issues : []
+                            });
+                        }
+
+                    });
+                });
+            });
+
+
+            var arr = [];
+            var _x;
+            var _y;
+            var _suites;
+            var _cases;
+            for (var k in arrObj) {
+                if (arrObj.hasOwnProperty(k)) {
+                    _x = arrObj[k];
+                    _suites = [];
+                    for (var l in _x.suites) {
+                        if (_x.suites.hasOwnProperty(l)) {
+
+                            _y = _x.suites[l];
+                            _cases = [];
+                            for (var m in _y.testCases) {
+                                if (_y.testCases.hasOwnProperty(m)) {
+                                    _cases.push(_y.testCases[m]);
+                                }
+                            }
+                            _y.testCases = _cases;
+                            _suites.push(_y);
+                        }
+                    }
+                    _x.suites = _suites;
+                    arr.push(_x);
+                }
+            }
+
+            var _pRateRound = Math.round(100 * 10 * passedTc / totalTc) / 10;
+            var _fRateRound = Math.round(100 * 10 * failedTc / totalTc) / 10;
+            var _pRateRound2 = Math.round(100 * 10 * passedWithIssues / totalTc) / 10;
+            var _sRateRound = Math.round(10 * (100 - _pRateRound - _fRateRound - _pRateRound2)) / 10;
+
+            var start = $scope.getDate($scope.run.start);
+            var stop = $scope.getDate($scope.run.end);
+
+            $scope.overview = {
+                totalTc: totalTc,
+                pRate: _pRateRound + "% passed, " + _pRateRound2 + "% passed with issues, " + _fRateRound + "% failed and " + _sRateRound + "% skipped",
+                testers: _users.length,
+                duration: getDuration(stop.getTime() - start.getTime()),
+                time: totalMins > 59 ? Math.floor(totalMins / 60) + " hours and " + (totalMins % 60) + " minutes" : totalMins + " minutes"
+            };
+            $scope.specs = arr;
+            $scope.labels = _allLabels;
+        }
+
+
+        function getDuration(duration) {
+
+            var seconds = Math.floor(duration / 1000);
+            var minutes = Math.floor(seconds / 60);
+            var hours = Math.floor(minutes / 60);
+            var days = Math.floor(hours / 24);
+            hours = hours - (days * 24);
+            minutes = minutes - (days * 24 * 60) - (hours * 60);
+            seconds = seconds - (days * 24 * 60 * 60) - (hours * 60 * 60) - (minutes * 60);
+
+            var result = "";
+            if (days > 0) {
+                result += (days + " days|");
+            }
+            if (hours > 0) {
+                result += (hours + " hours|");
+            }
+            if (minutes > 0) {
+                result += (minutes + " minutes|");
+            }
+            if (seconds > 0) {
+                result += (seconds + " seconds|");
+            }
+
+            result = result.replace(/\|/g, " ");
+            if (result.length === 0) {
+                result = "0 seconds";
+            }
+
+            return result;
+        }
+
+        $scope.filter = function () {
+            var _v;
+            $scope.specs.forEach(function (spec) {
+                spec.suites.forEach(function (suite) {
+                    suite.testCases.forEach(function (tcase) {
+                        tcase.results.forEach(function (result) {
+                            _v = shouldDisplay(result);
+                            _v = _v && ($scope.selectedLabels.length > 0 ? $scope.selectedLabels.indexOf(spec.label) > -1 : true);
+                            result.visible = _v;
+                        });
+                    });
+                });
+            });
+
+
+
+
+
+        };
+
+        function shouldDisplay(result) {
+            return $scope.resultFilter[result.resultClass];
+        }
+
+        function getResult(tc) {
+            if (tc.result === "passed" && tc.issues.length > 0) {
+                return "passed with issues";
+            } else {
+                return tc.result;
+            }
+        }
+
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+
+    function RunCtrlUser($scope, $location, $routeParams, runHttp, assignmentHttp, attachmentHttp, reviewHttp, SynergyUtils, SynergyHandlers, SynergyIssue) {
+
+        $scope.$emit("updateNavbar", {item: "nav_runs"});
+        $scope.id = $routeParams.id || -1;
+        $scope.platforms = [];
+        $scope.run = {};
+        $scope.rights = 0;
+        var leaderIsRemoving = false;
+        $scope.explainModal = "";
+        var currentActionId = -1;
+        var currentAction = "";
+        $scope.username = $scope.SYNERGY.session.username || "";
+        $scope.attachmentBase = $scope.SYNERGY.server.buildURL("run_attachment", {});
+        $scope.assignees = [];
+        $scope.specifications = [];
+        $scope.tribes = [];
+        $scope.project = {"name": "", "id": -1};
+        var tribes = [];
+        $scope.filter = {
+            "assignee": "All",
+            "specification": "All",
+            "tribe": "All"
+        };
+        $scope.coverage = [];
+        var _coverage = [];
+        $scope.isLoading = false;
+        $scope.pageSize = $scope.SYNERGY.assignmentPage;
+        $scope.sortConfig = {
+            property: ["id"],
+            descending: [false]
+        };
+        $scope.P1Issues = [];
+        $scope.P2Issues = [];
+        $scope.P3Issues = [];
+        $scope.unresolvedIssues = [];
+        $scope.allIssues = [];
+        $scope.orderingProperties = {
+            "userDisplayName": {
+                "desc": false,
+                "asc": false
+            },
+            "specification": {
+                "desc": false,
+                "asc": false
+            }
+        };
+        $scope.pageReviewsExpanded = false;
+        $scope.testRunIsAvailable = false;
+        var issueCollector = new SynergyIssue.RunIssuesCollector();
+
+        /**
+         * Loads test run
+         */
+        $scope.fetch = function () {
+            issueCollector = new SynergyIssue.RunIssuesCollector();
+            $scope.P1Issues = [];
+            tribes = [];
+            _coverage = [];
+            $scope.project = {"name": "", "id": -1};
+            $scope.P2Issues = [];
+            $scope.P3Issues = [];
+            $scope.unresolvedIssues = [];
+            $scope.allIssues = [];
+            $scope.isLoading = true;
+            $scope.coverage = [];
+            $scope.tribes = [];
+            $scope.platforms = [];
+            $scope.assignees = [];
+            $scope.specifications = [];
+            $scope.run = {};
+            $scope.username = $scope.SYNERGY.session.username || "";
+            runHttp.getUserCentric($scope, $scope.id, function (data) {
+                setTestRunIsAvailable(data);
+                $scope.run = data;
+                setProject(data);
+                $scope.$emit("updateBreadcrumbs", {link: "run/" + $scope.id + "/v/2", title: $scope.run.title});
+                getPlatforms();
+                buildAssignments();
+                SynergyUtils.ProgressChart([((data.completed / data.total) * 100), 100 - ((data.completed / data.total) * 100)], ["#08c", "#ccc"], ["completed", ""], "canvas2");
+                try {
+                    if (data.controls.length > 0) {
+                        $scope.rights = 1;
+                    }
+                } catch (e) {
+                }
+                $scope.isLoading = false;
+            }, $scope.generalHttpFactoryError);
+        };
+
+        $scope.toggleReivewSection = function () {
+            $scope.pageReviewsExpanded = !$scope.pageReviewsExpanded;
+            $("#pageReviews").collapse("toggle");
+        };
+
+        function setTestRunIsAvailable(testRun) {
+            var start = $scope.getDate(testRun.start);
+            var stop = $scope.getDate(testRun.end);
+            var today = new Date();
+            $scope.testRunIsAvailable = (today >= start && today <= stop);
+        }
+
+        function setProject(data) {
+            if (data.projectName !== null) {
+                $scope.project = {"name": data.projectName, "id": data.projectId};
+            } else {
+                $scope.project = {"name": $scope.SYNERGY.product, "id": -2};
+            }
+        }
+
+        $scope.assignessFilter = function (assignmentRecord) {
+            if ($scope.filter.assignee && $scope.filter.assignee !== "All" && assignmentRecord.userDisplayName !== $scope.filter.assignee) {
+                return false;
+            }
+            if ($scope.filter.specification && $scope.filter.specification !== "All" && assignmentRecord.specification !== $scope.filter.specification) {
+                return false;
+            }
+            if ($scope.filter.tribe && $scope.filter.tribe !== "All" && assignmentRecord.tribes.indexOf($scope.filter.tribe) < 0) {
+                return false;
+            }
+            return true;
+        };
+
+        $scope.nextPage = function () {
+            $scope.isLoading = true;
+            $scope.pageSize += $scope.SYNERGY.assignmentPage;
+            $scope.isLoading = false;
+        };
+
+        $scope.bugs = [];
+        $scope.bugsAssignmentId = -1;
+
+        $scope.alterBugs = function (assignment) {
+            $scope.bugsAssignmentId = assignment.id;
+            var a = [];
+            for (var i = 0, max = assignment.issues.length; i < max; i++) {
+                a.push({
+                    "id": assignment.issues[i].bugId,
+                    "stillValid": true,
+                    "changeCount": false
+                });
+            }
+            $scope.bugs = a;
+            $("#ticketsModal").modal("toggle");
+        };
+
+        $scope.performAlterBugs = function () {
+            var newIssues = [];
+            var countDiff = 0;
+            for (var i = 0, max = $scope.bugs.length; i < max; i++) {
+                if ($scope.bugs[i].stillValid) {
+                    newIssues.push($scope.bugs[i].id);
+                } else {
+                    if ($scope.bugs[i].changeCount) {
+                        countDiff++;
+                    }
+                }
+            }
+
+            assignmentHttp.alterBugs($scope, $scope.bugsAssignmentId, {issues: newIssues.join(";"), diffCount: countDiff}, function () {
+                $scope.SYNERGY.logger.log("Done", "Issues updated", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+
+
+            $("#ticketsModal").modal("toggle");
+        };
+
+        function resetFilters() {
+            $scope.sortConfig = {
+                "property": ["id"],
+                "descending": [false]
+            };
+            $scope.orderingProperties = {
+                "userDisplayName": {
+                    "desc": false,
+                    "asc": false
+                },
+                "specification": {
+                    "desc": false,
+                    "asc": false
+                }
+            };
+        }
+
+        $scope.changeSorting = function (prop, order) {
+            var _p = prop;
+            prop = order ? prop : "-" + prop;
+            if ($scope.sortConfig.property.indexOf(_p) > -1 || $scope.sortConfig.property.indexOf("-" + _p) > -1) {
+                var i = $scope.sortConfig.property.indexOf(_p);
+                if (i < 0) {
+                    i = $scope.sortConfig.property.indexOf("-" + _p);
+                }
+                if ($scope.sortConfig.descending[i] === order) { // click on the same order arrow => remove it from filter
+                    $scope.sortConfig.property.splice(i, 1);
+                    $scope.sortConfig.descending.splice(i, 1);
+                    $scope.orderingProperties[_p].desc = false; // reset css for this property
+                    $scope.orderingProperties[_p].asc = false;
+                    if ($scope.sortConfig.property.length === 0) { // resel css to default values and reset filter to ID 
+                        resetFilters();
+                    }
+                } else {
+                    $scope.orderingProperties[_p].desc = !$scope.orderingProperties[_p].desc; // invert css
+                    $scope.orderingProperties[_p].asc = !$scope.orderingProperties[_p].asc;
+                    var _orig = {// just place holder
+                        "descending": !$scope.sortConfig.descending[i],
+                        "property": $scope.sortConfig.property[i]
+                    };
+                    $scope.sortConfig.descending.splice(i, 1); // remove it from array of filters
+                    $scope.sortConfig.property.splice(i, 1);
+                    $scope.sortConfig.descending.splice(0, 0, _orig.descending); // insert it to level of filters at the beginning
+                    (_orig.property.indexOf("-") === 0) ? $scope.sortConfig.property.splice(0, 0, _orig.property.substring(1, _orig.property.length)) : $scope.sortConfig.property.splice(0, 0, "-" + _orig.property);
+                }
+            } else {
+                if ($scope.sortConfig.property.length === 1 && $scope.sortConfig.property[0] === "id") { // if no ordering so far, simply replace ID with selected property
+                    $scope.sortConfig = {
+                        property: [prop],
+                        descending: [order]
+                    };
+                } else {
+                    $scope.sortConfig.property.splice(0, 0, prop); // insert at the beginning
+                    $scope.sortConfig.descending.splice(0, 0, order);
+                }
+
+                $scope.orderingProperties[_p].desc = !order; // css update
+                $scope.orderingProperties[_p].asc = order;
+            }
+        };
+
+        $scope.createCoverageChart = function (c) {
+            try {
+                SynergyUtils.ProgressChart([c.progress, 100 - c.progress], ["#08c", "#ccc"], ["finished", ""], "coverage" + c.name);
+            } catch (e) {
+            }
+        };
+
+        function buildAssignments() {
+            var result = {
+                "failed": 0,
+                "passed": 0,
+                "skipped": 0
+            };
+            var assignees = [];
+            var specs = [];
+            tribes = [];
+            var duplicateAssignments = [];
+            var prettyAssignments = [];
+            var allResolved;
+            for (var username in $scope.run.assignments) {
+                if ($scope.run.assignments.hasOwnProperty(username)) {
+                    var user = $scope.run.assignments[username];
+                    var line = {
+                        assignments: []
+                    };
+                    for (var assign = 0, maxAssign = user.assignments.length; assign < maxAssign; assign++) {
+                        var assignment = user.assignments[assign];
+                        line.userDisplayName = assignment.userDisplayName;
+                        line.username = assignment.username;
+                        line.specificationId = assignment.specificationId;
+                        line.label = assignment.label;
+                        line.labelId = assignment.labelId;
+                        line.specification = assignment.specification;
+                        line.tribes = assignment.tribes;
+                        assignees.indexOf(line.userDisplayName) < 0 && assignees.push(line.userDisplayName);
+                        specs.indexOf(line.specification) < 0 && specs.push(line.specification);
+                        getTribes(assignment);
+                        assignment.total = parseInt(assignment.total, 10);
+                        assignment.completed = parseInt(assignment.completed, 10);
+                        assignment.failedColor = "#62c462";
+                        allResolved = allIssuesResolved(assignment.issues);
+                        if (parseInt(assignment.failed, 10) > 0 && !allResolved) {
+                            assignment.failedColor = "#ee5f5b";
+                        }
+                        if (assignment.total > 0) {
+                            assignment.progress = Math.round(100 * 10 * assignment.completed / assignment.total) / 10;
+                            assignment.progressLabel = "Completed " + assignment.progress + "%";
+                        } else {
+                            assignment.progressLabel = "No cases to test";
+                            assignment.progress = 100;
+                        }
+                        assignment.info = (allResolved && assignment.progress === 100 && parseInt(assignment.failed, 10) === 0) ? "finished" : assignment.info;
+                        var _t = (assignment.started.length > 0 ? "Started: " + assignment.started : "");
+                        _t += (assignment.lastUpdated.length > 0 ? "; Last updated: " + assignment.lastUpdated : "");
+                        assignment.tooltip = assignment.total > 0 ? _t : "At the time of assignment creation, there were no matching cases. You can try to start this assignment to see if situation has changed";
+                        if (assignment.issues.length === 1) {
+                            assignment.issuesLink = (assignment.issues.length > 0) ? "<a style='color: " + assignment.failedColor + "; font-weight: bold' href='" + $scope.SYNERGY.issues.viewLink($scope.project.name, assignment.issues) + "'>" + assignment.issues.length + " issue</a>&nbsp;" : "&nbsp;";
+                        } else if (assignment.issues.length > 1) {
+                            assignment.issuesLink = (assignment.issues.length > 0) ? "<a style='color: " + assignment.failedColor + "; font-weight: bold' href='" + $scope.SYNERGY.issues.viewLink($scope.project.name, assignment.issues) + "'>" + assignment.issues.length + " issues</a>&nbsp;" : "&nbsp;";
+                        }
+                        if (typeof line.assignments[$scope.platforms.indexOf(assignment.platform)] !== "undefined") {
+                            line.assignments[$scope.platforms.indexOf(assignment.platform)].duplicates = true;
+                            assignment.duplicates = true;
+                            duplicateAssignments.push(assignment);
+                        } else {
+                            assignment.duplicates = false;
+                            line.assignments[$scope.platforms.indexOf(assignment.platform)] = assignment;
+                        }
+                        if (!_coverage[$scope.platforms.indexOf(assignment.platform)]) { // FIXME
+                            _coverage[$scope.platforms.indexOf(assignment.platform)] = {total: 0, completed: 0, name: assignment.platform};
+                        }
+
+                        _coverage[$scope.platforms.indexOf(assignment.platform)].total += parseInt(assignment.total, 10);
+                        _coverage[$scope.platforms.indexOf(assignment.platform)].completed += parseInt(assignment.completed, 10);
+
+                        result.failed += Math.floor(assignment.failed);
+                        result.passed += Math.floor(assignment.passed);
+                        result.skipped += Math.floor(assignment.skipped);
+                    }
+
+                    for (var p = 0, maxp = $scope.platforms.length; p < maxp; p++) {
+                        if (!line.assignments[p]) {
+                            line.assignments[p] = {"_hidden": new Date()};
+                        }
+                    }
+
+                    prettyAssignments.push(line);
+                }
+            }
+
+            // duplicates
+            for (var dupl = 0, maxa = duplicateAssignments.length; dupl < maxa; dupl++) {
+                prettyAssignments.push(getLineForDupliciteAssignment(duplicateAssignments[dupl]));
+            }
+
+            assignees.push("All");
+            assignees.sort(function (a, b) {
+                return a.toLowerCase() < b.toLowerCase() ? -1 : 1;
+            });
+            specs.push("All");
+            specs.sort(function (a, b) {
+                return a.toLowerCase() < b.toLowerCase() ? -1 : 1;
+            });
+            tribes.push("All");
+            tribes.sort(function (a, b) {
+                return a.toLowerCase() < b.toLowerCase() ? -1 : 1;
+            });
+            $scope.assignees = assignees;
+            $scope.specifications = specs;
+            $scope.tribes = tribes;
+            countResults(result);
+            $scope.prettyAssignments = prettyAssignments;
+            SynergyUtils.ProgressChart([issueCollector.issuesStats.opened / (issueCollector.issuesStats.total / 100), 100 - (issueCollector.issuesStats.opened / (issueCollector.issuesStats.total / 100))], ["#ccc", "#62c462"], ["Unresolved", "Resolved"], "issuesResolution");
+            SynergyUtils.ProgressChart([issueCollector.issuesStats.unknown / (issueCollector.issuesStats.total / 100), issueCollector.issuesStats.P1 / (issueCollector.issuesStats.total / 100), issueCollector.issuesStats.P2 / (issueCollector.issuesStats.total / 100), issueCollector.issuesStats.P3 / (issueCollector.issuesStats.total / 100), issueCollector.issuesStats.P4 / (issueCollector.issuesStats.total / 100)], ["#E8EBAB", "#ee5f5b", "#f89406", "#fbeed5", "#ccc"], ["Unknown (" + issueCo [...]
+            $scope.allIssues = issueCollector.issues;
+            $scope.unresolvedIssues = issueCollector.issuesStats.unresolvedIssues;
+            $scope.P1Issues = issueCollector.issuesStats.P1Issues;
+            $scope.P2Issues = issueCollector.issuesStats.P2Issues;
+            $scope.P3Issues = issueCollector.issuesStats.P3Issues;
+        }
+
+        function getLineForDupliciteAssignment(assignment) {
+            var line = {
+                assignments: []
+            };
+            line.userDisplayName = assignment.userDisplayName;
+            line.username = assignment.username;
+            line.specificationId = assignment.specificationId;
+            line.label = assignment.label;
+            line.labelId = assignment.labelId;
+            line.specification = assignment.specification;
+            line.tribes = assignment.tribes;
+            line.assignments[$scope.platforms.indexOf(assignment.platform)] = assignment;
+            for (var p = 0, maxp = $scope.platforms.length; p < maxp; p++) {
+                if (!line.assignments[p]) {
+                    line.assignments[p] = {"_hidden": new Date()};
+                }
+            }
+            return line;
+        }
+
+        function allIssuesResolved(issues) {
+            var result = true;
+            for (var i = 0, max = issues.length; i < max; i++) {
+                issueCollector.addIssue(issues[i]);
+                if (issues[i].status.toLowerCase() !== "resolved" && issues[i].status.toLowerCase() !== "closed" && issues[i].status.toLowerCase() !== "verified") {
+                    result = false;
+                }
+            }
+            return result;
+        }
+
+        function getTribes(assignment) {
+            for (var i = 0, max = assignment.tribes.length; i < max; i++) {
+                if (tribes.indexOf(assignment.tribes[i]) < 0) {
+                    tribes.push(assignment.tribes[i]);
+                }
+            }
+        }
+
+        // retrieves all distinct platforms from all assignments and assign them to $scope.platforms
+        function getPlatforms() {
+            for (var username in $scope.run.assignments) {
+                if ($scope.run.assignments.hasOwnProperty(username)) {
+                    var user = $scope.run.assignments[username];
+                    for (var assignment in user.assignments) {
+                        if ($scope.platforms.indexOf(user.assignments[assignment].platform) < 0) {
+                            $scope.platforms.push(user.assignments[assignment].platform);
+                        }
+                    }
+                }
+            }
+            $scope.platforms.sort();
+        }
+        /**
+         * Counts number of passed/failed/skipped cases
+         * @param {TestRun} run
+         */
+        function countResults(result) {
+            var t = (result.failed + result.passed + result.skipped) / 100;
+            if (t > 0) {
+                var f = Math.floor(result.failed * 10 / t) / 10;
+                var p = Math.round(result.passed * 10 / t) / 10;
+                SynergyUtils.ProgressChart([p, f, Math.round(10 * (100 - (f + p))) / 10], ["#62c462", "#ee5f5b", "#c67605"], ["passed", "failed", "skipped"], "canvas1");
+            }
+
+            for (var i = 0, max = _coverage.length; i < max; i++) {
+                _coverage[i].progress = Math.round(10 * 100 * (_coverage[i].completed / _coverage[i].total)) / 10;
+            }
+            $scope.coverage = _coverage;
+
+        }
+
+        /**
+         * Redirects to page so user starts testing and completing his assignments
+         * @param {Number} mode
+         * @param {Number} assignmentId assignment ID
+         */
+        $scope.startAssignment = function (mode, assignmentId) {
+            if (parseInt(mode, 10) === 2) {// restart => show modal confirmation
+                $("#deleteModalLabel").text("Restart assignment?");
+                $("#deleteModalBody").html("<p>Do you really want to restart this assignment? All saved progress will be lost as if you never started it. If you want to Continue saved assignment, please use 'Play' button instead</p>");
+                $("#deleteModal").modal("toggle");
+                currentAction = "restartAssignment";
+                currentActionId = assignmentId;
+            } else {
+                $location.path("/assignment/" + assignmentId + "/v/" + mode);
+            }
+        };
+
+        /**
+         * Starts with action on given test run. If the action name is different than "delete", redirection is done.
+         * Otherwise confirmation dialog is opened
+         * @param {String} action action name
+         */
+        $scope.performRun = function (action) {
+            switch (action) {
+                case "delete":
+                    $("#deleteModalLabel").text("Delete test run?");
+                    $("#deleteModalBody").html("<p>Do you really want to delete test run?</p>");
+                    $("#deleteModal").modal("toggle");
+                    currentAction = "deleteRun";
+                    break;
+                case "notify":
+                    $("#deleteModalLabel").text("Send notifications?");
+                    $("#deleteModalBody").html("<p>Do you really want to send email notifications to testers with incomplete test assignment?</p>");
+                    $("#deleteModal").modal("toggle");
+                    currentAction = "notify";
+                    break;
+                case "freeze":
+                    var target = ($scope.run.isActive ? 0 : 1);
+                    runHttp.freezeRun($scope, $scope.id, target, function (data) {
+                        $scope.run.isActive = target;
+                        (target === 1) ? $scope.SYNERGY.logger.log("Done", "Test run unfrozen", "INFO", "alert-success") : $scope.SYNERGY.logger.log("Done", "Test run frozen", "INFO", "alert-success");
+                    }, $scope.generalHttpFactoryError);
+                    break;
+                default:
+                    $location.path("/administration/run/" + $scope.id + "/" + action);
+                    break;
+            }
+        };
+
+        /**
+         * Starts with action on given run attachment
+         * Otherwise confirmation dialog is opened
+         * @param {String} action action name
+         * @param {Number} id attachment ID
+         */
+        $scope.performAttachment = function (action, id) {
+            switch (action) {
+                case "delete":
+                    $("#deleteModalLabel").text("Delete attachment?");
+                    $("#deleteModalBody").html("<p>Do you really want to delete attachment?</p>");
+                    $("#deleteModal").modal("toggle");
+                    currentAction = "deleteAttachment";
+                    currentActionId = id;
+                    break;
+                default:
+                    break;
+            }
+        };
+
+        /**
+         * Redirects user to page where he can create a new run assignment
+         */
+        $scope.forwardToCreateAssignment = function () {
+            $location.path("/administration/assignment/create/run/" + $scope.id);
+        };
+
+        /**
+         * Redirects user to page where he can create a new matrix run assignment
+         */
+        $scope.forwardToCreateMatrixAssignment = function () {
+            $location.path("/administration/assignment/creatematrix/run/" + $scope.id);
+        };
+
+        /**
+         * Starts with action on given test assignment
+         * Otherwise confirmation dialog is opened
+         * @param {String} action action name
+         * @param {Number} id assignment ID
+         */
+        $scope.performAssignment = function (action, id, createdBy) {
+            if (action !== "delete") {
+                $location.path("suite/" + id + "/" + action);
+            } else {
+                switch (action) {
+                    case "delete":
+                        leaderIsRemoving = (createdBy === 3) ? true : false;
+                        $("#deleteModalLabel").text("Delete test assignment?");
+                        $("#deleteModalBody").html("<p>Do you really want to delete test assignment?</p>");
+                        $("#deleteModal").modal("toggle");
+                        currentAction = "deleteAssignment";
+                        currentActionId = id;
+                        break;
+                    default:
+                        break;
+                }
+            }
+        };
+
+        $scope.deleteAssignment = function () {
+            if (!leaderIsRemoving) {
+                if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                    return;
+                }
+                assignmentHttp.remove($scope, currentActionId, function (data) {
+                    $scope.SYNERGY.logger.log("Done", "Assignment deleted", "INFO", "alert-success");
+                    $scope.fetch();
+                }, $scope.generalHttpFactoryError);
+            } else {
+                $("#explainModal").modal("toggle");
+                if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1 || $scope.explanation.length < 1) {
+                    return;
+                }
+                assignmentHttp.removeByLeader($scope, currentActionId, $scope.explanation, function (data) {
+                    $scope.SYNERGY.logger.log("Done", "Assignment deleted", "INFO", "alert-success");
+                    $scope.fetch();
+                }, $scope.generalHttpFactoryError);
+            }
+        };
+
+        $scope.startReviewAssignment = function (mode, assignmentId) {
+            if (parseInt(mode, 10) === 2) {// restart => show modal confirmation
+                $("#deleteModalLabel").text("Restart assignment?");
+                $("#deleteModalBody").html("<p>Do you really want to restart this assignment? All saved comments will be lost as if you never started it. If you want to Continue saved assignment, please use 'Play' button instead</p>");
+                $("#deleteModal").modal("toggle");
+                currentAction = "restartReviewAssignment";
+                currentActionId = assignmentId;
+            } else {
+                $location.path("/review/" + assignmentId + "/continue");
+            }
+        };
+
+        $scope.performReviewAssignment = function (action, id, createdBy) {
+            switch (action) {
+                case "delete":
+                    currentAction = "deleteReviewAssignment";
+                    currentActionId = id;
+                    leaderIsRemoving = (createdBy === 3) ? true : false;
+                    $("#deleteModalLabel").text("Delete review assignment?");
+                    $("#deleteModalBody").html("<p>Do you really want to delete review assignment?</p>");
+                    $("#deleteModal").modal("toggle");
+                    break;
+                default:
+                    $location.path("/review/" + id + "/" + action);
+                    break;
+            }
+        };
+
+        function deleteReviewAssignment() {
+            if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                return;
+            }
+            reviewHttp.remove($scope, currentActionId, function (data) {
+                $scope.SYNERGY.logger.log("Done", "Assignment deleted", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+        }
+        /**
+         * Executes some action based on value of $scope.currentAction
+         */
+        $scope.performAction = function () {
+            switch (currentAction) {
+                case "restartAssignment":
+                    $("#deleteModal").modal("toggle");
+                    $location.path("/assignment/" + currentActionId + "/v/2");
+                    break;
+                case "deleteAssignment":
+                    $("#deleteModal").modal("toggle");
+                    leaderIsRemoving ? $("#explainModal").modal("toggle") : $scope.deleteAssignment();
+                    break;
+                case "restartReviewAssignment":
+                    $("#deleteModal").modal("toggle");
+                    $location.path("/review/" + currentActionId + "/restart");
+                    break;
+                case "deleteReviewAssignment":
+                    $("#deleteModal").modal("toggle");
+                    deleteReviewAssignment();
+                    break;
+                case "notify":
+                    $("#deleteModal").modal("toggle");
+                    runHttp.sendNotifications($scope, $scope.id, function (data) {
+                        $scope.SYNERGY.logger.log("Done", data, "INFO", "alert-success");
+                    }, $scope.generalHttpFactoryError);
+                    break;
+                case "deleteRun":
+                    $("#deleteModal").modal("toggle");
+                    if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                        return;
+                    }
+                    runHttp.remove($scope, $scope.id, function (data) {
+                        $scope.SYNERGY.modal.update("Test run removed", "");
+                        $scope.SYNERGY.modal.show();
+                        $location.path("/runs");
+                    }, function (data) {
+                        $scope.SYNERGY.modal.update("Action failed", "");
+                        $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+                        $scope.SYNERGY.modal.show();
+                    });
+                    break;
+                case "deleteAttachment":
+                    $("#deleteModal").modal("toggle");
+                    if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                        return;
+                    }
+                    attachmentHttp.removeRunAttachment($scope, currentActionId, function (data) {
+                        $scope.SYNERGY.logger.log("Done", "Attachment deleted", "INFO", "alert-success");
+                        $scope.fetch();
+                    }, function (data) {
+                        $scope.SYNERGY.logger.log("Action failed", "", "INFO", "alert-error");
+                        $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+                        $scope.fetch();
+                    });
+                    break;
+                default:
+                    break;
+            }
+        };
+
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+
+        // ATTACHMENT UPLOAD HANDLING
+        new SynergyHandlers.FileUploader([], "dropbox", $scope.SYNERGY.uploadFileLimit, $scope.SYNERGY.server.buildURL("run_attachment", {"id": $scope.id}), function (title, msg, level, style, fileName) {
+            $scope.SYNERGY.logger.log(title, msg, level, style);
+            $scope.fileName = fileName;
+            $scope.fetch();
+        }, function (title, msg, level, style) {
+            $scope.SYNERGY.logger.log(title, msg, level, style);
+        });
+    }
+    /**
+     * @param {VersionsFct} versionsHttp
+     * @param {SpecificationFct} specificationHttp description
+     * @param {SuiteFct} suiteHttp description
+     * @param {AttachmentFct} attachmentHttp description
+     * @param {UsersFct} usersHttp description
+     *  * @param {JobFct} jobHttp description
+     */
+    function SpecificationCtrl($scope, utils, $location, $routeParams, versionsHttp, specificationHttp, suiteHttp, attachmentHttp, usersHttp, jobHttp, userHttp, sanitizerHttp, labelsHttp, projectsHttp, specificationCache, SynergyUtils, SynergyModels, SynergyHandlers) {//authService
+
+        var self = this;
+        $scope.project = null;
+        $scope.$emit("updateNavbar", {item: "nav_specs"});
+        $scope.specification = {};
+        $scope.refreshCodemirror = false; // on edit/create page it is necessary to refresh editor element
+        $scope.id = $routeParams.id || -1;
+        $scope.rights = 0;
+        $scope.readOnlyJobs = [];
+        $scope.users = []; // used on edit page to select owner
+        $scope.filename = "";
+        $scope.attachmentBase = $scope.SYNERGY.server.buildURL("attachment", {});
+        $scope.versions = [];
+        $scope.version = "";
+        $scope.newLabel = "";
+        $scope.removeLabel = "";
+        self.simpleName = $routeParams.simpleName || "";
+        self.simpleVersion = $routeParams.simpleVersion || "";
+        self.originalSimpleName = "";
+        $scope.keepSimpleNameTrack = true;
+        $scope.filterLabel = $routeParams.label || "All";
+        $scope.labels = [];
+        $scope.realLabels = []; // without all
+        $scope.removalUsers = "";
+        $scope.requestMsg = "";
+        $scope.projects = [];
+        var specificationDurationCache = {};
+        var currentAction = "";
+        var currentActionId = -1;
+        var currentSuiteId = -1;
+
+        /**
+         * Loads data from server
+         */
+        $scope.fetch = function (useCache) {
+            if (self.simpleName.length > 0) {
+                loadSpecificationFromAlias();
+                return;
+            }
+
+            if (window.location.href.indexOf("/title/") > -1) {// shouldn't be, caused by some issue in .htaccess
+                $location.path("");
+                return;
+            }
+
+            switch (getAction()) {
+                case "create":
+                    versionsHttp.get($scope, true, function (data) {
+                        $scope.versions = data;
+                        $scope.version = data[0].name || "";
+                        $scope.refreshCodemirror = true;
+                    }, $scope.generalHttpFactoryError);
+
+                    projectsHttp.getAll($scope, function (data) {
+                        $scope.projects = data;
+                        $scope.project = $scope.projects[0];
+                    }, $scope.generalHttpFactoryError);
+
+                    break;
+                case "1":
+                    if ($scope.id < 0) {
+                        return;
+                    }
+                    if (specificationCache.getCurrentSpecificationId() === parseInt($scope.id, 10)) {
+                        displaySimpleSpecification(specificationCache.getCurrentSpecification());
+                    } else {
+                        specificationCache.resetCurrentSpecification();
+                        specificationHttp.get($scope, useCache, $scope.id, function (data) {
+                            displaySimpleSpecification(data);
+                        }, $scope.generalHttpFactoryError);
+                    }
+                    break;
+                case "2":
+                    if ($scope.id < 0) {
+                        return;
+                    }
+                    specificationHttp.getFull($scope, useCache, $scope.id, function (data) {
+                        $scope.specification = data;
+                        setProject(data);
+                        specificationDurationCache.All = data.estimation;
+                        specificationCache.setCurrentSpecification(data, $scope.project);
+                        getRemovalUsers();
+                        $scope.labels = getLabels(data);
+                        $scope.newname = data.title;
+                        resolveContinuousJobs(data.ext.continuous_integration);
+                        $scope.$emit("updateBreadcrumbs", {link: "specification/" + $scope.id + "/v/2", title: data.title});
+                        try {
+                            if (data.controls.length > 0) {
+                                $scope.rights = 1;
+                            }
+                        } catch (e) {
+                        }
+
+                    }, $scope.generalHttpFactoryError);
+                    break;
+                default : // edit
+                    if ($scope.id < 0) {
+                        return;
+                    }
+                    if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                        break;
+                    }
+
+                    specificationHttp.get($scope, false, $scope.id, function (data) {
+                        self.originalSimpleName = data.simpleName;
+                        setProject(data);
+                        $scope.refreshCodemirror = true;
+                        if (SynergyUtils.definedNotNull(data.ext.continuous_integration)) {
+                            for (var j = 0, max = data.ext.continuous_integration.length; j < max; j++) {
+                                data.ext.continuous_integration[j].jobUrl = data.ext.continuous_integration[j].jobUrl.substring(0, data.ext.continuous_integration[j].jobUrl.indexOf("/lastCompletedBuild"));
+                            }
+                        }
+                        $scope.$emit("updateBreadcrumbs", {link: "specification/" + $scope.id, title: data.title});
+                        try {
+                            if (data.controls.length > 0) {
+                                $scope.rights = 1;
+                            }
+                        } catch (e) {
+                        }
+                        $scope.specification = data;
+                        $scope.specification.originalOwner = data.owner;
+
+                        projectsHttp.getAll($scope, function (data) {
+
+                            var defaultProject = true;
+                            for (var p = 0, maxp = data.length; p < maxp; p++) {
+                                if (data[p].name === $scope.project.name) {
+                                    $scope.project.id = data[p].id;
+                                    defaultProject = false;
+                                    break;
+                                }
+                            }
+                            if (defaultProject) {
+                                data.push($scope.project);
+                            }
+                            $scope.projects = data;
+
+                        }, $scope.generalHttpFactoryError);
+
+                        usersHttp.getAll($scope, function (data) {
+                            $scope.users = data.users;
+                        }, $scope.generalHttpFactoryError);
+                    }, $scope.generalHttpFactoryError);
+
+                    break;
+            }
+        };
+
+        function displaySimpleSpecification(data) {
+            $scope.specification = data;
+            $scope.newname = data.title;
+            setProject(data);
+            resolveContinuousJobs(data.ext.continuous_integration);
+            getRemovalUsers();
+            $scope.$emit("updateBreadcrumbs", {link: "specification/" + $scope.id + "/v/1", title: data.title});
+            try {
+                if (data.controls.length > 0) {
+                    $scope.rights = 1;
+                }
+            } catch (e) {
+            }
+        }
+
+        function setProject(data) {
+            if (data.hasOwnProperty("ext") && data.ext.hasOwnProperty("projects") && data.ext.projects.length > 0) {
+                $scope.project = data.ext.projects[0];
+            } else {
+                $scope.project = {"name": $scope.SYNERGY.product, id: -2};
+            }
+        }
+
+        /**
+         * Sets all users that requested specification removal to $scope.removalUsers
+         */
+        function getRemovalUsers() {
+            var _l = "";
+            for (var i = 0, max = $scope.specification.ext.removalRequests.length; i < max; i++) {
+                _l += $scope.specification.ext.removalRequests[i].username + ", ";
+            }
+            $scope.removalUsers = (_l.length === 1) ? "" : _l.substr(0, _l.length - 2);
+        }
+
+        /**
+         * Returns action based on URL (edit, create, 1,2)
+         * @returns {String} action
+         */
+        function getAction() {
+            var url = window.location.href;
+            var stringId = $scope.id + "";
+            var _s = url.lastIndexOf("/" + $scope.id + "/") + stringId.length + 2;
+            var _e = url.indexOf("/", _s);
+            var action = (_e > -1) ? url.substring(_s, _e) : url.substring(_s, url.length);
+            if (action === "v") {
+                return (url.indexOf($scope.id + "/v/1") > -1) ? "1" : "2";
+            }
+            return action;
+        }
+
+        $scope.getSpecificationDuration = function () {
+            if (!$scope.filterLabel || $scope.filterLabel === "All") {
+                return $scope.specification.estimation;
+            }
+
+            if (typeof specificationDurationCache[$scope.filterLabel] !== "undefined") {
+                return specificationDurationCache[$scope.filterLabel];
+            }
+
+            var time = 0;
+            for (var i = 0, max = $scope.specification.testSuites.length; i < max; i++) {
+                for (var j = 0, max2 = $scope.specification.testSuites[i].testCases.length; j < max2; j++) {
+                    for (var k = 0, max3 = $scope.specification.testSuites[i].testCases[j].keywords.length; k < max3; k++) {
+                        if ($scope.specification.testSuites[i].testCases[j].keywords[k] === $scope.filterLabel) {
+                            time += $scope.specification.testSuites[i].testCases[j].duration;
+                        }
+                    }
+                }
+            }
+            specificationDurationCache[$scope.filterLabel] = time;
+            return time;
+        }
+
+        /**
+         * Returns true or false if test case matches label filter
+         * @param {type} testCase
+         * @returns {Boolean} true if test case has given label or if searched label is set to All, false if it doesn't 
+         */
+        $scope.hasLabel = function (testCase, a) {
+            if (!$scope.filterLabel || $scope.filterLabel === "All") {
+                return true;
+            }
+            for (var i = 0, max = testCase.keywords.length; i < max; i++) {
+                if (testCase.keywords[i] === $scope.filterLabel) {
+                    return true;
+                }
+            }
+            return false;
+        };
+        /**
+         * Asks server to sanitize input and renders it to the Preview tab
+         */
+        $scope.loadPreview = function () {
+            var _t = "<h1>" + ($scope.specification.title || "") + "</h1><h3>Description</h3><div class='well'>" + ($scope.specification.desc || "") + "</div>";
+            sanitizerHttp.getSanitizedInput($scope, _t, function (data) {
+                $scope.preview = data;
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Toggle state of specification in user's favorites list
+         */
+        $scope.toggleFavorite = function () {
+            var target = parseInt($scope.specification.isFavorite, 10) > 0 ? 0 : 1;
+            var spec = new SynergyModels.Specification("", "", "", "", $scope.id);
+            spec.isFavorite = target;
+            userHttp.toggleFavorite($scope, spec, function (data) {
+                $scope.specification.isFavorite = target;
+                if (target === 0) {
+                    $scope.SYNERGY.logger.log("Done", "Specification removed from favorites", "INFO", "alert-success");
+                } else {
+                    $scope.SYNERGY.logger.log("Done", "Specification added to favorites", "INFO", "alert-success");
+                }
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Loads specification based on simple name property in URL
+         * @returns {undefined}
+         */
+        function loadSpecificationFromAlias() {
+            self.simpleName = decodeURIComponent(decodeURIComponent(self.simpleName));
+            specificationHttp.getFullAlias($scope, false, self.simpleName, self.simpleVersion, function (data) {
+                $scope.specification = data;
+                $scope.labels = getLabels(data);
+                specificationDurationCache.All = data.estimation;
+                setProject(data);
+                specificationCache.setCurrentSpecification(data, $scope.project);
+                $scope.newname = data.title;
+                $scope.id = data.id;
+                resolveContinuousJobs(data.ext.continuous_integration);
+                $scope.$emit("updateBreadcrumbs", {link: "specification/" + data.id + "/v/2", title: data.title});
+                try {
+                    if (data.controls.length > 0) {
+                        $scope.rights = 1;
+                    }
+                } catch (e) {
+                }
+            }, $scope.generalHttpFactoryError);
+        }
+
+        /**
+         * Collects all distinct labels from specification, adds "All" label and sorts them alphabetically
+         * @param {Specification} data
+         * @returns {Array} array of strings
+         */
+        function getLabels(data) {
+            var labels = [];
+            var realLabels = [];
+            for (var i = 0, max = data.testSuites.length; i < max; i++) {
+                for (var j = 0, max2 = data.testSuites[i].testCases.length; j < max2; j++) {
+                    for (var k = 0, max3 = data.testSuites[i].testCases[j].keywords.length; k < max3; k++) {
+                        if (labels.indexOf(data.testSuites[i].testCases[j].keywords[k]) < 0) {
+                            labels.push(data.testSuites[i].testCases[j].keywords[k]);
+                            realLabels.push(data.testSuites[i].testCases[j].keywords[k]);
+                        }
+                    }
+                }
+            }
+
+            labels.sort(function (a, b) {
+                return a.toLowerCase() < b.toLowerCase() ? -1 : 1;
+            });
+            realLabels.sort(function (a, b) {
+                return a.toLowerCase() < b.toLowerCase() ? -1 : 1;
+            });
+            $scope.realLabels = realLabels;
+            if ($scope.realLabels.length > 0) {
+                $scope.removeLabel = $scope.realLabels[0];
+            }
+            labels.push("All");
+            return labels;
+        }
+
+        /**
+         * Resolve each continuous job
+         */
+        function resolveContinuousJobs(jobs) {
+
+            function doResolve(data) {
+                $scope.readOnlyJobs.push(data);
+            }
+
+            function logError(data) {
+                $scope.SYNERGY.logger.log("Failed to load data", data, "DEBUG", "alert-error");
+            }
+
+            $scope.readOnlyJobs = [];
+            if (!SynergyUtils.definedNotNull(jobs)) {
+                return;
+            }
+            for (var i = 0, max = jobs.length; i < max; i++) {
+                jobHttp.resolve($scope, jobs[i], doResolve, logError);
+            }
+        }
+
+        /**
+         * General attachment action
+         * @param {String} action
+         * @param {Number} id attachment ID
+         */
+        $scope.performAttachment = function (action, id) {
+            switch (action) {
+                case "delete":
+                    $("#deleteModalLabel").text("Delete attachment?");
+                    $("#deleteModalBody").html("<p>Do you really want to delete attachment?</p>");
+                    $("#modal_confirm_ok").attr("ng-click", "deleteAttachment()");
+                    $("#deleteModal").modal("toggle");
+                    currentAction = "deleteAttachment";
+                    currentActionId = id;
+                    break;
+                default:
+                    $location.path("specification_attachment/" + id + "/" + action);
+                    break;
+            }
+        };
+
+        $scope.showJobsModal = function () {
+            $("#jobsModal").modal("toggle");
+        };
+
+        /**
+         * Removes job from specification
+         */
+        $scope.removeJob = function (jobId) {
+            jobHttp.remove($scope, jobId, $scope.specification.id, function (data) {
+                for (var i = 0, max = $scope.specification.ext.continuous_integration.length; i < max; i++) {
+                    if ($scope.specification.ext.continuous_integration[i].id === jobId) {
+                        $scope.specification.ext.continuous_integration.splice(i, 1);
+                        break;
+                    }
+                }
+                $scope.SYNERGY.modal.update("Job removed", "");
+                $scope.SYNERGY.modal.show();
+            }, function (data) {
+                $scope.SYNERGY.modal.show();
+                $scope.SYNERGY.logger.log("Action failed", data, "INFO", "alert-error");
+                $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+            });
+        };
+        /**
+         * Adds job to specification
+         */
+        $scope.addJob = function () {
+            var job = new SynergyModels.Job($scope.jobToBeAdded, $scope.id, -1);
+            jobHttp.create($scope, job, function (data) {
+                $scope.specification.ext.continuous_integration.push(job);
+                $scope.SYNERGY.modal.update("Job added", "");
+                $scope.SYNERGY.modal.show();
+            }, $scope.generalHttpFactoryError);
+        };
+        /**
+         * Clones specification (sends request to server)
+         * @returns {unresolved}
+         */
+        $scope.clone = function () {
+            if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                return;
+            }
+            if (!$scope.newname) {
+                $scope.newname = $scope.specification.title;
+            }
+            if ($scope.newname.length < 1 || $scope.cloneVersion.length < 1) {
+                $scope.SYNERGY.modal.update("Missing parameters", "Please add a new name and version");
+                $scope.SYNERGY.modal.show();
+            } else {
+                specificationHttp.clone($scope, currentActionId, $scope.newname, $scope.cloneVersion, function (newLink) {
+                    var id = newLink.substring(newLink.indexOf("=") + 1);
+                    $scope.SYNERGY.logger.log("Done", "Specification created, see " + window.location.hostname + window.location.pathname + "#/specification/" + id, "INFO", "alert-success");
+                }, $scope.generalHttpFactoryError);
+            }
+        };
+
+        /**
+         * Starts with action on given test suite
+         * Otherwise confirmation dialog is opened
+         * @param {String} action action name
+         * @param {Number} id suite ID
+         */
+        $scope.performSuite = function (action, id) {
+            // delete could be done on a same page
+            switch (action) {
+                case "delete":
+                    $("#deleteModalLabel").text("Delete test suite?");
+                    $("#deleteModalBody").html("<p>Do you really want to delete test suite?</p>");
+                    $("#deleteModal").modal("toggle");
+                    currentAction = "deleteSuite";
+                    currentActionId = id;
+                    break;
+                case "labels":
+                    //  $("#addLabelsModalLabel").text("Add label to all cases in suite");
+                    $("#addLabelsModal").modal("toggle");
+                    currentAction = "labels";
+                    currentActionId = id;
+                    break;
+                default:
+                    $location.path("suite/" + id + "/" + action);
+                    break;
+            }
+
+        };
+
+        $scope.performCase = function (action, id, suiteId) {
+            switch (action) {
+                case "delete":
+                    $("#deleteModalLabel").text("Delete test case?");
+                    $("#deleteModalBody").html("<p>This will only remove reference to this test case in this suite. Continue?</p>");
+                    $("#deleteModal").modal("toggle");
+                    currentAction = "deleteCase";
+                    currentActionId = id;
+                    currentSuiteId = suiteId;
+                    break;
+                default:
+                    break;
+            }
+        };
+
+        $scope.deleteCase = function () {
+            $("#deleteModal").modal("toggle");
+            if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                return;
+            }
+            specificationCache.resetCurrentSpecification();
+            var toBeRemovedCaseId = parseInt(currentActionId, 10);
+            var toBeRemovedSuiteId = parseInt(currentSuiteId, 10);
+            suiteHttp.removeCase($scope, currentSuiteId, currentActionId, function (data) {
+                $scope.SYNERGY.logger.log("Done", "Test Case removed from test suite", "INFO", "alert-success");
+                for (var i = 0, max = $scope.specification.testSuites.length; i < max; i++) {
+                    if ($scope.specification.testSuites[i].id === toBeRemovedSuiteId) {
+                        var s = $scope.specification.testSuites[i];
+                        for (var j = 0, max2 = s.testCases.length; j < max2; j++) {
+                            if (s.testCases[j].id === toBeRemovedCaseId) {
+                                s.testCases.splice(j, 1);
+                                return;
+                            }
+                        }
+
+                    }
+                }
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Executes some action based on value of $scope.currentAction
+         */
+        $scope.performAction = function (labelMode) {
+            switch (currentAction) {
+                case "deleteAttachment":
+                    $scope.deleteAttachment();
+                    break;
+                case "cloneSpecification":
+                    $scope.clone();
+                    break;
+                case "ownershipRequest":
+                    specificationHttp.requestOwnership($scope, new SynergyModels.OwnershipRequest($scope.id, $scope.SYNERGY.session.username, $scope.requestMsg), function () {
+                        $scope.SYNERGY.logger.log("Done", "Request has been sent to owner", "INFO", "alert-success");
+                        $scope.fetch();
+                    }, $scope.generalHttpFactoryError);
+                    break;
+                case "deleteSpecification":
+                    $scope.deleteSpecification();
+                    break;
+                case "deleteSuite":
+                    $("#deleteModal").modal("toggle");
+                    if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                        return;
+                    }
+                    specificationCache.resetCurrentSpecification();
+                    var toBeRemovedSuiteId = parseInt(currentActionId, 10);
+                    suiteHttp.remove($scope, currentActionId, function () {
+                        $scope.SYNERGY.logger.log("Done", "Test suite deleted", "INFO", "alert-success");
+                        for (var i = 0, max = $scope.specification.testSuites.length; i < max; i++) {
+                            if ($scope.specification.testSuites[i].id === toBeRemovedSuiteId) {
+                                $scope.specification.testSuites.splice(i, 1);
+                                return;
+                            }
+                        }
+                    }, $scope.generalHttpFactoryError);
+                    break;
+                case "deleteCase":
+                    $scope.deleteCase();
+                    break;
+                case "labels":
+                    if (labelMode === "add") {
+                        addLabels();
+                    } else {
+                        removeLabels();
+                    }
+                    break;
+                default:
+                    break;
+            }
+        };
+
+        /**
+         * Removes label from suite and then removes it from $scope.specification
+         */
+        function removeLabels() {
+            if ($scope.removeLabel.length < 1) {
+                return;
+            }
+            var id = parseInt(currentActionId, 10);
+            specificationCache.resetCurrentSpecification();
+            labelsHttp.removeFromSuite($scope, currentActionId, $scope.removeLabel, function () {
+                $scope.SYNERGY.logger.log("Done", "Labels removed", "INFO", "alert-success");
+                for (var i = 0, max = $scope.specification.testSuites.length; i < max; i++) {
+                    if (parseInt($scope.specification.testSuites[i].id, 10) === id) {
+                        for (var j = 0, max2 = $scope.specification.testSuites[i].testCases.length; j < max2; j++) {
+                            var index = $scope.specification.testSuites[i].testCases[j].keywords.indexOf($scope.removeLabel);
+                            if (index > -1) {
+                                $scope.specification.testSuites[i].testCases[j].keywords.splice(index, 1);
+                            }
+                        }
+                        break;
+                    }
+                }
+                $scope.labels = getLabels($scope.specification);
+            }, $scope.generalHttpFactoryError);
+            $("#addLabelsModal").modal("toggle");
+        }
+
+        function addLabels() {
+            if ($scope.newLabel.length < 1) {
+                return;
+            }
+            var id = parseInt(currentActionId, 10);
+            specificationCache.resetCurrentSpecification();
+            labelsHttp.createForSuite($scope, currentActionId, $scope.newLabel, function () {
+                $scope.SYNERGY.logger.log("Done", "Labels added", "INFO", "alert-success");
+                for (var i = 0, max = $scope.specification.testSuites.length; i < max; i++) {
+                    if (parseInt($scope.specification.testSuites[i].id, 10) === id) {
+                        for (var j = 0, max2 = $scope.specification.testSuites[i].testCases.length; j < max2; j++) {
+                            if ($scope.specification.testSuites[i].testCases[j].keywords.indexOf($scope.newLabel) < 0) {
+                                $scope.specification.testSuites[i].testCases[j].keywords.push($scope.newLabel);
+                            }
+                        }
+                        break;
+                    }
+                }
+                $scope.labels = getLabels($scope.specification);
+            }, $scope.generalHttpFactoryError);
+            $("#addLabelsModal").modal("toggle");
+        }
+
+        /**
+         * Removes attachment (calls server)
+         * @returns {unresolved}
+         */
+        $scope.deleteAttachment = function () {
+            $("#deleteModal").modal("toggle");
+            if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                return;
+            }
+            var toBeRemoved = currentActionId;
+            attachmentHttp.removeSpecAttachment($scope, currentActionId, $scope.specification.id, function (data) {
+                $scope.SYNERGY.logger.log("Done", "Attachment deleted", "INFO", "alert-success");
+                for (var i = 0, max = $scope.specification.attachments.length; i < max; i++) {
+                    if ($scope.specification.attachments[i].id === toBeRemoved) {
+                        $scope.specification.attachments.splice(i, 1);
+                        return;
+                    }
+                }
+            }, function (data) {
+                $scope.SYNERGY.logger.log("Action failed", data, "INFO", "alert-error");
+                $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+                $scope.fetch();
+            });
+        };
+
+        /**
+         * Executes some action with specification based on value of $scope.currentAction
+         */
+        $scope.performSpecification = function (action) {
+            // delete could be done on a same page
+            switch (action) {
+                case "delete":
+                    $("#deleteModalLabel").text("Delete specification?");
+                    $("#deleteModalBody").html("<p>Do you really want to delete specification?</p>");
+                    $("#deleteModal").modal("toggle");
+                    currentAction = "deleteSpecification";
+                    break;
+                case "clone":
+                    currentActionId = $scope.id;
+                    currentAction = "cloneSpecification";
+                    versionsHttp.get($scope, true, function (data) {
+                        $scope.versions = data;
+                        $("#dupliciteSpecModal").modal("toggle");
+                    }, $scope.generalHttpFactoryError);
+                    break;
+                case "ownershipRequest":
+                    if ($scope.SYNERGY.session.username.length < 1) {
+                        return;
+                    }
+                    $("#ownershipRequestModal").modal("toggle");
+                    currentActionId = $scope.id;
+                    currentAction = "ownershipRequest";
+                    break;
+                default:
+                    $location.path("specification/" + $scope.specification.id + "/" + action);
+                    break;
+            }
+
+        };
+
+        /**
+         * Goes back in history by 1 step
+         */
+        $scope.cancel = function () {
+            window.history.back();
+        };
+
+        /**
+         * Deletes specification (calls server)
+         * @returns {unresolved}
+         */
+        $scope.deleteSpecification = function () {
+            $("#deleteModal").modal("toggle");
+            if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                return;
+            }
+            specificationCache.resetCurrentSpecification();
+            specificationHttp.remove($scope, $scope.specification.id, function (data, status) {
+                if (status === 202) {
+                    $scope.SYNERGY.logger.log("Done", "Request to remove this specification has been sent to owner", "INFO", "alert-success");
+                    $scope.specification.ext.removalRequests.push({"username": $scope.SYNERGY.session.username});
+                    getRemovalUsers();
+                } else {
+                    $scope.SYNERGY.modal.update("Specification deleted", "");
+                    $scope.SYNERGY.modal.show();
+                    $location.path("specifications");
+                }
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Saves modified specification
+         */
+        $scope.save = function () {
+            if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                return;
+            }
+            var spec = new SynergyModels.Specification($scope.specification.title, $scope.specification.desc, "", $scope.specification.owner, $scope.specification.id);
+            var intProjectId = parseInt($scope.project.id, 10);
+            spec.ext.projects = [{"name": $scope.projects.filter(function (item, index) {
+                        if (parseInt(item.id, 10) === intProjectId) {
+                            return item.name;
+                        }
+                    })[0].name, "id": intProjectId}];
+            spec.setSimpleName($scope.specification.simpleName);
+            if ($scope.myForm.$invalid || typeof spec.desc === "undefined" || spec.desc.length < 0) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+            specificationCache.resetCurrentSpecification();
+            specificationHttp.edit($scope, spec, $scope.minorEdit ? true : false, $scope.keepSimpleNameTrack, function (data) {
+                $scope.SYNERGY.modal.update("Specification updated", "");
+                $scope.SYNERGY.modal.show();
+                $location.path("specification/" + $scope.specification.id);
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Creates a new specification
+         * @returns {unresolved}
+         */
+        $scope.create = function () {
+            if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                return;
+            }
+            if ($scope.myForm.$invalid || typeof $scope.specification.desc === "undefined" || $scope.specification.desc.length < 0) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+            specificationCache.resetCurrentSpecification();
+            var spec = new SynergyModels.Specification($scope.specification.title, $scope.specification.desc, $scope.version, "", -1);
+            var intProjectId = parseInt($scope.project.id, 10);
+            spec.ext.projects = [{"name": $scope.projects.filter(function (item, index) {
+                        if (parseInt(item.id, 10) === intProjectId) {
+                            return item.name;
+                        }
+                    })[0].name, "id": intProjectId}];
+            spec.setSimpleName($scope.specification.title);
+            specificationHttp.create($scope, spec, function (data) {
+                $scope.SYNERGY.modal.update("Specification created", "");
+                var id = data.substring(data.indexOf("=") + 1, data.length - 1);
+                $location.path("specification/" + id);
+                $scope.SYNERGY.modal.show();
+            }, $scope.generalHttpFactoryError);
+        };
+
+// ATTACHMENT UPLOAD
+        new SynergyHandlers.FileUploader([], "dropbox", $scope.SYNERGY.uploadFileLimit, $scope.SYNERGY.server.buildURL("attachment", {"id": $scope.id, "type": "specification"}), function (title, msg, level, style, fileName) {
+            $scope.SYNERGY.logger.log(title, msg, level, style);
+            $scope.fileName = fileName;
+            specificationCache.resetCurrentSpecification();
+            attachmentHttp.getAttachmentsForSpecification($scope, $scope.specification.id, function (data) {
+                $scope.specification.attachments = data;
+            }, function (data) {
+                $scope.SYNERGY.logger.log("Failed to refresh list of attachments", data, "INFO", "alert-error");
+                $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+                try {
+                    if (!$scope.$$phase) {
+                        $scope.$apply();
+                    }
+                } catch (e) {
+                }
+            });
+        }, function (title, msg, level, style) {
+            $scope.SYNERGY.logger.log("Action failed", msg, "INFO", "alert-error");
+            $scope.SYNERGY.logger.log(title, msg, level, style);
+            try {
+                if (!$scope.$$phase) {
+                    $scope.$apply();
+                }
+            } catch (e) {
+            }
+        });
+
+        $scope.uploadFile = function () {
+            new SynergyHandlers.FileUploader([], "dropbox", $scope.SYNERGY.uploadFileLimit, $scope.SYNERGY.server.buildURL("attachment", {"id": $scope.id, "type": "specification"}), function (title, msg, level, style, fileName) {
+                $scope.SYNERGY.logger.log(title, msg, level, style);
+                $scope.fileName = fileName;
+                specificationCache.resetCurrentSpecification();
+                attachmentHttp.getAttachmentsForSpecification($scope, $scope.specification.id, function (data) {
+                    $scope.specification.attachments = data;
+                }, function (data) {
+                    $scope.SYNERGY.logger.log("Failed to refresh list of attachments", data, "INFO", "alert-error");
+                    $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+                    try {
+                        if (!$scope.$$phase) {
+                            $scope.$apply();
+                        }
+                    } catch (e) {
+                    }
+                });
+            }, function (title, msg, level, style) {
+
+                $scope.SYNERGY.logger.log("Action failed", msg, "INFO", "alert-error");
+                $scope.SYNERGY.logger.log(title, msg, level, style);
+                try {
+                    if (!$scope.$$phase) {
+                        $scope.$apply();
+                    }
+                } catch (e) {
+                }
+            }).uploadFileFromFileChooser("fileToUpload");
+        };
+        var scope = $scope;
+        $scope.init(function () {
+            scope.fetch(true);
+        });
+    }
+    /**
+     * 
+     * @param {SpecificationFct} specificationHttp
+     * @param {SuiteFct} suiteHttp
+     * @param {CasesFct} casesHttp
+     * @returns {undefined}
+     */
+    function SuiteCtrl($scope, utils, $location, $routeParams, $timeout, specificationHttp, suiteHttp, casesHttp, productsHttp, sanitizerHttp, specificationCache, SynergyModels) {//authService
+        $scope.$emit("updateNavbar", {item: "nav_specs"});
+        $scope.suite = {};
+        $scope.project = "";
+        $scope.refreshCodemirror = false;
+        $scope.id = $routeParams.id || -1;
+        $scope.rights = 0;
+        // for create only
+        $scope.c_version = $routeParams.version || ""; // used on create page to display version
+        $scope.c_specificationId = $routeParams.specification || ""; // used on create page to display specification link
+        $scope.c_specification = {}; // used on create page to display specification title
+        $scope.case_suggestions = []; // matching cases when adding existing case to suite
+        $scope.caseToBeAdded = ""; // the name that user types in add existing case dialog
+        $scope.availableProducts = false;
+        $scope.products = [];
+        $scope.oldNotification = "";
+        $scope.components = [];
+
+        var currentAction = "";
+        var currentActionId = -1;
+
+        /**
+         * Loads data from server
+         */
+        $scope.fetch = function () {
+            var action = window.location + "";
+            action = action.substring(action.lastIndexOf("/") + 1);
+            switch (action) {
+                case "create":
+                    if (parseInt($scope.c_specificationId, 10) > 0) {
+                        specificationHttp.get($scope, true, $scope.c_specificationId, function (data) {
+                            setLastSuiteOrder(data);
+                            setProject(data);
+                            $scope.c_specification = data;
+                            loadProducts();
+                        }, $scope.generalHttpFactoryError);
+                    }
+                    break;
+                case "1":
+                    if ($scope.id < 0) {
+                        return;
+                    }
+                    var cachedSuite = specificationCache.getCurrentSuite(parseInt($scope.id, 10));
+                    if (cachedSuite) {
+                        $scope.suite = cachedSuite;
+                        $scope.project = specificationCache.getCurrentProjectName();
+                        $scope.$emit("updateBreadcrumbs", {link: "suite/" + $scope.id + "/v/1", title: $scope.suite.title});
+                        try {
+                            if ($scope.suite.controls.length > 0) {
+                                $scope.rights = 1;
+                            }
+                        } catch (e) {
+                        }
+                        return;
+                    }
+
+
+
+                    suiteHttp.get($scope, true, $scope.id, function (data) {
+                        $scope.suite = data;
+                        setProject(data);
+                        $scope.$emit("updateBreadcrumbs", {link: "suite/" + $scope.id + "/v/1", title: data.title});
+                        try {
+                            if (data.controls.length > 0) {
+                                $scope.rights = 1;
+                            }
+                        } catch (e) {
+                        }
+                    }, $scope.generalHttpFactoryError);
+                    break;
+                default :
+                    if ($scope.id < 0) {
+                        return;
+                    }
+                    if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                        break;
+                    }
+                    suiteHttp.get($scope, false, $scope.id, function (data) {
+                        $scope.suite = data;
+                        setProject(data);
+                        $scope.refreshCodemirror = true;
+                        try {
+                            if (data.controls.length > 0) {
+                                $scope.rights = 1;
+                            }
+                        } catch (e) {
+                        }
+
+                        loadProducts();
+
+                    }, $scope.generalHttpFactoryError);
+                    break;
+            }
+        };
+
+        function setProject(data) {
+            if (data.hasOwnProperty("ext") && data.ext.hasOwnProperty("projects") && data.ext.projects.length > 0) {
+                $scope.project = data.ext.projects[0].name;
+            } else {
+                $scope.project = $scope.SYNERGY.product;
+            }
+        }
+
+        function setLastSuiteOrder(data) {
+            try {
+                $scope.suite.order = (data.testSuites[data.testSuites.length - 1].order + 1) || 1;
+            } catch (e) {
+                $scope.suite.order = 1;
+            }
+        }
+        /**
+         * Loads sanitized preview from server
+         */
+        $scope.loadPreview = function () {
+            var _t = "<h1>" + ($scope.suite.title || "") + "</h1><h3>Setup</h3><div class='well'>" + ($scope.suite.desc || "") + "</div>";
+            sanitizerHttp.getSanitizedInput($scope, _t, function (data) {
+                $scope.preview = data;
+            }, $scope.generalHttpFactoryError);
+        };
+
+        function loadProducts() {
+            productsHttp.get($scope, function (data) {
+                if (data.length > 0) {
+                    var p = [];
+                    for (var j = 0, max2 = data.length; j < max2; j++) {
+                        p.push(new SynergyModels.Product(data[j].name, data[j].components));
+                    }
+                    $scope.products = p;
+                    $scope.availableProducts = true;
+
+                    var oldPreferences = $scope.SYNERGY.cache.get("product_component");
+                    if (oldPreferences && (($scope.suite.product === "unknown" && $scope.suite.component === "unknown") || (typeof $scope.suite.product === "undefined"))) {
+                        for (var i = 0, max = $scope.products.length; i < max; i++) {
+                            if (data[i].name === oldPreferences.product) {
+                                $scope.suite.product = $scope.products[i];//select current product in form
+                                $scope.suite.component = oldPreferences.component;
+                                setComponent(i);
+                                $scope.oldNotification = "Selected product/component are based on previously used values and do not match actual settings of this suite";
+                                return;
+                            }
+                        }
+                    } else {
+                        for (var i = 0, max = $scope.products.length; i < max; i++) {
+                            if (data[i].name === $scope.suite.product || typeof $scope.suite.product === "undefined") {
+                                $scope.suite.product = $scope.products[i];//select current product in form
+                                setComponent(i);
+                                return;
+                            }
+                        }
+                    }
+                    $scope.suite.product = $scope.products[0];
+                    setComponent(0);
+
+                }
+            }, function () {
+            });
+        }
+
+        function setComponent(productIndex) {
+            $scope.components = $scope.products[productIndex].components;
+            for (var i = 0, max = $scope.components.length; i < max; i++) {
+                if ($scope.components[i].name === $scope.suite.component || typeof $scope.suite.component === "undefined" || (typeof $scope.suite.component.name !== "undefined" && $scope.components[i].name === $scope.suite.component.name)) {
+                    $scope.suite.component = $scope.components[i];
+                    return;
+                }
+            }
+
+            $scope.suite.component = $scope.components[0];
+        }
+
+        $scope.productChanged = function () {
+            for (var i = 0, max = $scope.products.length; i < max; i++) {
+                if ($scope.products[i].name === $scope.suite.product.name) {
+                    $scope.suite.product = $scope.products[i];//select current product in form
+                    setComponent(i);
+                    break;
+                }
+            }
+        };
+
+        /**
+         * Starts with action on given test case
+         * Otherwise confirmation dialog is opened
+         * @param {String} action action name
+         * @param {Number} id suite ID
+         */
+        $scope.performCase = function (action, id) {
+            switch (action) {
+                case "delete":
+                    $("#deleteModalLabel").text("Delete test case?");
+                    $("#deleteModalBody").html("<p>This will only remove reference to this test case in this suite. Continue?</p>");
+                    $("#deleteModal").modal("toggle");
+                    currentAction = "deleteCase";
+                    currentActionId = id;
+                    break;
+                default:
+                    $location.path("case/" + id + "/suite/" + $scope.id + "/" + action);
+                    break;
+            }
+        };
+
+        /**
+         * Executes some action based on value of currentAction
+         */
+        $scope.performAction = function () {
+            switch (currentAction) {
+                case "deleteCase":
+                    $scope.deleteCase();
+                    break;
+                case "deleteSuite":
+                    $scope.deleteSuite();
+                    break;
+                default:
+                    break;
+            }
+        };
+
+        /**
+         * Removes test case
+         */
+        $scope.deleteCase = function () {
+            $("#deleteModal").modal("toggle");
+            if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                return;
+            }
+            specificationCache.resetCurrentSpecification();
+            var caseToBeRemovedId = parseInt(currentActionId, 10);
+            suiteHttp.removeCase($scope, $scope.suite.id, currentActionId, function (data) {
+                $scope.SYNERGY.logger.log("Done", "Test Case removed from test suite", "INFO", "alert-success");
+                for (var i = 0, max = $scope.suite.testCases.length; i < max; i++) {
+                    if ($scope.suite.testCases[i].id === caseToBeRemovedId) {
+                        $scope.suite.testCases.splice(i, 1);
+                        return;
+                    }
+                }
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Adds case to suite
+         * @param {type} caseId
+         */
+        $scope.addCase = function (caseId) {
+            $("#addCaseModal").modal("toggle");
+            specificationCache.resetCurrentSpecification();
+            suiteHttp.addCase($scope, $scope.id, caseId, function (data) {
+                $scope.SYNERGY.logger.log("Done", "Test Case added to test suite", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Starts with action on given test suite
+         * Otherwise confirmation dialog is opened
+         * @param {String} action action name
+         * @param {Number} id suite ID
+         */
+        $scope.performSuite = function (action) {
+            switch (action) {
+                case "delete":
+                    $("#deleteModalLabel").text("Delete test suite?");
+                    $("#deleteModalBody").html("<p>Do you really want to delete test suite?</p>");
+                    $("#deleteModal").modal("toggle");
+                    currentAction = "deleteSuite";
+                    break;
+                default:
+                    $location.path("suite/" + $scope.suite.id + "/" + action);
+                    break;
+            }
+        };
+
+        $scope.cancel = function () {
+            window.history.back();
+        };
+
+        /**
+         * Deletes test suite
+         */
+        $scope.deleteSuite = function () {
+            $("#deleteModal").modal("toggle");
+            if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                return;
+            }
+            specificationCache.resetCurrentSpecification();
+            suiteHttp.remove($scope, $scope.suite.id, function (data) {
+                $scope.SYNERGY.modal.update("Test Suite deleted", "");
+                $scope.SYNERGY.modal.show();
+                $location.path("specification/" + $scope.suite.specificationId);
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Saves modifications in suite
+         */
+        $scope.save = function () {
+            if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                return;
+            }
+            if ($scope.myForm.$invalid || $scope.myForm2.$invalid || typeof $scope.suite.desc === "undefined" || $scope.suite.desc.length < 0) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+            specificationCache.resetCurrentSpecification();
+            var product = ($scope.availableProducts) ? $scope.suite.product.name : $scope.suite.product;
+            var component = ($scope.availableProducts) ? $scope.suite.component.name : $scope.suite.component;
+            var suite = new SynergyModels.Suite($scope.suite.title, $scope.suite.desc, product, component, $scope.suite.id);
+            suite.order = $scope.suite.order;
+            suiteHttp.edit($scope, suite, $scope.minorEdit ? true : false, function (data) {
+                $scope.SYNERGY.modal.update("Test Suite updated", "");
+                $scope.SYNERGY.modal.show();
+                window.history.back();
+            }, $scope.generalHttpFactoryError);
+
+            if ($scope.availableProducts) {
+                $scope.SYNERGY.cache.put("product_component", {"product": $scope.suite.product.name, "component": $scope.suite.component.name});
+            }
+
+        };
+
+        /**
+         * Creates a new test suite
+         */
+        $scope.create = function () {
+            $scope.suite.specificationId = $scope.c_specificationId;
+
+            if ($scope.myForm.$invalid || $scope.myForm2.$invalid || typeof $scope.suite.desc === "undefined" || $scope.suite.desc.length < 0) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+
+            if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                return;
+            }
+            specificationCache.resetCurrentSpecification();
+            var product = ($scope.availableProducts) ? $scope.suite.product.name : $scope.suite.product;
+            var component = ($scope.availableProducts) ? $scope.suite.component.name : $scope.suite.component;
+            var suite = new SynergyModels.Suite($scope.suite.title, $scope.suite.desc, product, component, -1);
+            suite.order = $scope.suite.order;
+            suite.specificationId = $scope.suite.specificationId;
+            suiteHttp.create($scope, suite, function (data) {
+                $scope.SYNERGY.modal.update("Test Suite created", "");
+                $scope.SYNERGY.modal.show();
+                window.history.back();
+            }, $scope.generalHttpFactoryError);
+
+            if ($scope.availableProducts) {
+                $scope.SYNERGY.cache.put("product_component", {"product": $scope.suite.product.name, "component": $scope.suite.component});
+            }
+
+        };
+
+        /**
+         * Displays dialog for adding existing cases to suite
+         */
+        $scope.showAddCaseModal = function () {
+            $scope.case_suggestions = [];
+            $scope.caseToBeAdded = "";
+            $("#addCaseModal").modal("toggle");
+        };
+
+        /**
+         * Reduces list of offered cases based on $scope.caseToBeAdded
+         */
+        $scope.filterCases = function () {
+            $timeout(function () {
+                casesHttp.getMatching($scope, $scope.caseToBeAdded, function (data) {
+                    $scope.case_suggestions = data;
+                }, $scope.generalHttpFactoryError);
+            }, 600);
+        };
+// INIT
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+    /**
+     * 
+     * @param {SuiteFct} suiteHttp
+     * @param {CaseFct} caseHttp
+     * @param {ImageFct} imageHttp
+     * @returns {undefined} */
+    function CaseCtrl($scope, utils, $location, $routeParams, suiteHttp, caseHttp, imageHttp, issueHttp, labelHttp, sanitizerHttp, specificationCache, SynergyModels, SynergyHandlers) {//authService
+        $scope.$emit("updateNavbar", {item: "nav_specs"});
+        $scope.testCase = {};
+        $scope.project = "";
+        $scope.refreshCodemirror = false;
+        $scope.id = $routeParams.id || -1;
+        $scope.parentSuite = $routeParams.parent || -1;
+        $scope.rights = 0;
+        var currentAction = "";
+        var currentActionId = -1;
+        $scope.labelToBeAdded = "";
+        $scope.preview = ($scope.parentSuite < 0) ? 1 : 0; // whether or not the case is displayed without suite context
+        $scope.c_suite = {}; // used in create page to display suite information
+        var originalDuration = 0; // used in edit page, when user submits edited case, when submitted duration is different than originalDuration, server restarts duration count
+
+        $scope.loadPreview = function () {
+            var _t = "<h1>" + ($scope.testCase.title || "") + "</h1><h3>Steps</h3><div>" + ($scope.testCase.steps || "") + "</div><h3>Expected result:</h3><div class='result well'>" + ($scope.testCase.result || "") + "</div>";
+            sanitizerHttp.getSanitizedInput($scope, _t, function (data) {
+                $scope.preview = data;
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Loads data from server
+         */
+        $scope.fetch = function (useCache) {
+            var action = window.location + "";
+            action = action.substring(action.lastIndexOf("/") + 1);
+            switch (action) {
+                case "create":
+                    $scope.testCase.steps = "<ol>\n<li></li>\n<li></li>\n<li></li>\n<li></li>\n<ol>";
+                    $scope.testCase.duration = 1;
+                    if (parseInt($scope.parentSuite, 10) > 0) {
+                        suiteHttp.get($scope, useCache, $scope.parentSuite, function (data) {
+                            setProject(data);
+                            setLastCaseOrder(data);
+
+                            $scope.c_suite = data;
+                        }, $scope.generalHttpFactoryError);
+                    }
+                    break;
+                case "1":
+                    if ($scope.id < 0) {
+                        return;
+                    }
+                    var cachedCase = specificationCache.getCurrentCase(parseInt($scope.id, 10), parseInt($scope.parentSuite, 10));
+                    if (cachedCase) {
+                        $scope.testCase = cachedCase;
+                        $scope.project = specificationCache.getCurrentProjectName();
+                        $scope.$emit("updateBreadcrumbs", {link: "case/" + $scope.id + "/suite/" + $scope.parentSuite + "/v/1", title: $scope.testCase.title});
+                        try {
+                            if ($scope.testCase.controls.length > 0) {
+                                $scope.rights = 1;
+                            }
+                        } catch (e) {
+                        }
+                        return;
+                    }
+
+                    caseHttp.get($scope, useCache, $scope.id, $scope.parentSuite, function (data) {
+                        $scope.testCase = data;
+                        setProject(data);
+                        $scope.$emit("updateBreadcrumbs", {link: "case/" + $scope.id + "/suite/" + $scope.parentSuite + "/v/1", title: data.title});
+                        try {
+                            if (data.controls.length > 0) {
+                                $scope.rights = 1;
+                            }
+                        } catch (e) {
+                        }
+                    }, $scope.generalHttpFactoryError);
+                    break;
+                default :
+                    if ($scope.id < 0) {
+                        return;
+                    }
+                    if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                        break;
+                    }
+                    caseHttp.get($scope, false, $scope.id, $scope.parentSuite, function (data) {
+                        $scope.testCase = data;
+                        setProject(data);
+                        $scope.refreshCodemirror = true;
+                        $scope.testCase.suiteId = $scope.parentSuite;
+                        originalDuration = parseInt(data.duration, 10);
+                        try {
+                            if (data.controls.length > 0) {
+                                $scope.rights = 1;
+                            }
+                        } catch (e) {
+                        }
+                    }, $scope.generalHttpFactoryError);
+                    break;
+            }
+            // FIXME this is loaded twice on document load
+        };
+
+        function setProject(data) {
+            if (data.hasOwnProperty("ext") && data.ext.hasOwnProperty("projects") && data.ext.projects.length > 0) {
+                $scope.project = data.ext.projects[0].name;
+            } else {
+                $scope.project = $scope.SYNERGY.product;
+            }
+        }
+
+        /**
+         * When creating a new case, set order to be (previous case+1)
+         * @param {Suite} data
+         * @returns {undefined}
+         */
+        function setLastCaseOrder(data) {
+            try {
+                $scope.testCase.order = (data.testCases[data.testCases.length - 1].order + 1) || 1;
+            } catch (e) {
+                $scope.testCase.order = 1;
+            }
+        }
+
+        /**
+         * Creates a new test case
+         */
+        $scope.create = function () {
+            if (parseInt($scope.parentSuite, 10) < 1) {// parent suite HAS to be defined
+                return;
+            }
+            if ($scope.myForm.$invalid || $scope.myForm2.$invalid || typeof $scope.testCase.steps === "undefined" || typeof $scope.testCase.steps.length < 0 || typeof $scope.testCase.result === "undefined" || typeof $scope.testCase.result.length < 0) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+            specificationCache.resetCurrentSpecification();
+            var testCase = new SynergyModels.TestCase($scope.testCase.title, $scope.testCase.steps, $scope.testCase.result, $scope.testCase.duration, -1);
+            testCase.suiteId = $scope.parentSuite;
+            testCase.order = $scope.testCase.order;
+            if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                return;
+            }
+            caseHttp.create($scope, testCase, function (data) {
+                $scope.SYNERGY.modal.update("Test Case created", "");
+                $scope.SYNERGY.modal.show();
+                window.history.back();
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Based on $scope.currentAction, performs some action
+         */
+        $scope.performAction = function () {
+            switch (currentAction) {
+                case "deleteCase":
+                    $("#deleteModal").modal("toggle");
+                    if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1 || $scope.parentSuite < 1) {
+                        return;
+                    }
+                    specificationCache.resetCurrentSpecification();
+                    suiteHttp.removeCase($scope, $scope.parentSuite, $scope.id, function (data) {
+                        $scope.SYNERGY.modal.update("Test Case removed from test suite", "");
+                        $scope.SYNERGY.modal.show();
+                        window.history.back();
+                    }, $scope.generalHttpFactoryError);
+                    break;
+                case "deleteImage":
+                    var idToBeRemoved = currentActionId;
+                    specificationCache.resetCurrentSpecification();
+                    $("#deleteImageModal").modal("toggle");
+                    imageHttp.remove($scope, currentActionId, $scope.parentSuite, function (data) {
+                        $scope.SYNERGY.logger.log("Done", "Image removed", "INFO", "alert-success");
+                        for (var i = 0, max = $scope.testCase.images.length; i < max; i++) {
+                            if ($scope.testCase.images[i].id === idToBeRemoved) {
+                                $scope.testCase.images.splice(i, 1);
+                                return;
+                            }
+                        }
+                    }, $scope.generalHttpFactoryError);
+                    break;
+                default:
+                    break;
+            }
+        };
+
+        /**
+         * Starts with action on current test case
+         * Otherwise confirmation dialog is opened
+         * @param {String} action action name
+         */
+        $scope.performCase = function (action) {
+            // delete could be done on a same page
+            if ($scope.parentSuite < 0) {
+                return;
+            }
+            switch (action) {
+                case "delete":
+                    $("#deleteModalLabel").text("Delete test case?");
+                    $("#deleteModalBody").html("<p>This will only remove reference to this test case in this suite. Continue?</p>");
+                    $("#deleteModal").modal("toggle");
+                    currentAction = "deleteCase";
+                    break;
+                default:
+                    $location.path("case/" + $scope.testCase.id + "/suite/" + $scope.parentSuite + "/" + action);
+                    break;
+            }
+        };
+
+        /**
+         * Starts with action on given test case's image
+         * Otherwise confirmation dialog is opened
+         * @param {String} action action name
+         * @param {Number} id image ID
+         */
+        $scope.performImage = function (action, id) {
+            // delete could be done on a same page
+            if ($scope.parentSuite < 0) {
+                return;
+            }
+            switch (action) {
+                case "delete":
+                    $("#deleteImageModalLabel").text("Delete image?");
+                    $("#deleteImageModalBody").html("<p>This will delete image from this test case. Continue?</p>");
+                    $("#deleteImageModal").modal("toggle");
+                    currentAction = "deleteImage";
+                    currentActionId = id;
+                    break;
+                default:
+                    $location.path("image/" + id + "/" + action);
+                    break;
+            }
+
+        };
+
+        $scope.cancel = function () {
+            window.history.back();
+        };
+
+        /**
+         * Saves modified test case to server
+         * @param {Number} mode if 0, this test case will be cloned and modifications will be applied only to this suite, if 1 all suites will be affected
+         */
+        $scope.save = function (mode) {
+            if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                return;
+            }
+            if ($scope.myForm.$invalid || $scope.myForm2.$invalid) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+            specificationCache.resetCurrentSpecification();
+            var t_case = new SynergyModels.TestCase($scope.testCase.title, $scope.testCase.steps, $scope.testCase.result, $scope.testCase.duration, $scope.testCase.id);
+            t_case.suiteId = $scope.parentSuite;
+            t_case.order = $scope.testCase.order;
+            t_case.orginalDuration = originalDuration;
+            caseHttp.edit($scope, mode, t_case, $scope.minorEdit ? true : false, function (data) {
+                $scope.SYNERGY.modal.update("Test Case updated", "");
+                $scope.SYNERGY.modal.show();
+                window.history.back();
+            }, $scope.generalHttpFactoryError);
+        };
+        $scope.showAddIssueModal = function () {
+            $scope.issueToBeAdded = "";
+            $("#addIssueModal").modal("toggle");
+        };
+        $scope.addIssue = function () {
+            specificationCache.resetCurrentSpecification();
+            issueHttp.create($scope, {testCaseId: $scope.testCase.id, id: $scope.issueToBeAdded}, function (data) {
+                $scope.SYNERGY.logger.log("Issue added", "", "INFO", "alert-success");
+                $scope.testCase.issues.push({"bugId": $scope.issueToBeAdded, "title": "", "resolution": "", "id": -1});
+            }, $scope.generalHttpFactoryError);
+        };
+
+        $scope.removeIssue = function (id) {
+            specificationCache.resetCurrentSpecification();
+            issueHttp.remove($scope, {testCaseId: $scope.testCase.id, id: id}, function (data) {
+                $scope.SYNERGY.logger.log("Issue removed", "", "INFO", "alert-success");
+                for (var i = 0, max = $scope.testCase.issues.length; i < max; i++) {
+                    if ($scope.testCase.issues[i].bugId === id) {
+                        $scope.testCase.issues.splice(i, 1);
+                        return;
+                    }
+                }
+            }, $scope.generalHttpFactoryError);
+        };
+
+        $scope.showAddLabelModal = function () {
+            $scope.labelToBeAdded = "";
+            $("#addLabelModal").modal("toggle");
+        };
+        $scope.addLabel = function () {
+            specificationCache.resetCurrentSpecification();
+            labelHttp.create($scope, {"label": $scope.labelToBeAdded, "testCaseId": $scope.testCase.id, "suiteId": $scope.parentSuite}, function (data) {
+                $scope.SYNERGY.logger.log("Label added", "", "INFO", "alert-success");
+                $scope.testCase.keywords.push($scope.labelToBeAdded.toLowerCase());
+            }, $scope.generalHttpFactoryError);
+        };
+        $scope.removeLabel = function (label) {
+            specificationCache.resetCurrentSpecification();
+            labelHttp.remove($scope, {"label": label, "testCaseId": $scope.testCase.id, "suiteId": $scope.parentSuite}, function (data) {
+                $scope.SYNERGY.logger.log("Label removed", "", "INFO", "alert-success");
+                $scope.testCase.keywords.splice($scope.testCase.keywords.indexOf(label), 1);
+            }, $scope.generalHttpFactoryError);
+        };
+// INIT
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch(true);
+        });
+
+        // D&D images
+
+        var fu = new SynergyHandlers.FileUploader([], "fakeID", $scope.SYNERGY.uploadFileLimit, $scope.SYNERGY.server.buildURL("image", {"id": $scope.id, "suiteId": $scope.parentSuite, "title": encodeURIComponent($scope.imageTitle)}), function (title, msg, level, style, fileName) {
+            $scope.SYNERGY.logger.log(title, msg, level, style);
+            $scope.fileName = fileName;
+            specificationCache.resetCurrentSpecification();
+            imageHttp.getImagesForCase($scope, $scope.testCase.id, $scope.parentSuite, function (data) {
+                $scope.testCase.images = data;
+            }, function (data) {
+                $scope.SYNERGY.logger.log("Unable to refresh list of images", "", "INFO", "alert-error");
+                $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+            });
+        }, function (title, msg, level, style) {
+            $scope.SYNERGY.logger.log("Action failed", msg, "INFO", "alert-error");
+            $scope.SYNERGY.logger.log(title, msg, level, style);
+            try {
+                if (!$scope.$$phase) {
+                    $scope.$apply();
+                }
+            } catch (e) {
+            }
+        });
+        fu.initForImages("dropbox");
+
+        $scope.uploadFile = function () {
+            fu.uploadImage($scope.SYNERGY.server.buildURL("image", {"id": $scope.id, "suiteId": $scope.parentSuite, "title": encodeURIComponent($scope.imageTitle)}));
+        };
+
+    }
+    /**
+     * 
+     * @param {UserFct} userHttp
+     */
+    function ProfileCtrl($scope, $routeParams, userHttp, $location, SynergyModels, SynergyHandlers) {//authService
+        $scope.$emit("updateNavbar", {item: "nav_home"});
+        $scope.user = {authorOf: [], membership: [], favorites: [], assignments: []};
+        $scope.username = $routeParams.user || "";
+        $scope.rights = 0;
+        $scope.passwordChangeAllowed = !$scope.SYNERGY.useSSO;
+        $scope.updatePassword = false;
+
+        $scope.isLoggedIn = (typeof $scope.SYNERGY.session.session_id !== "undefined" && $scope.SYNERGY.session.session_id.length > 1) ? 1 : 0;
+        /**
+         * Loads data from server
+         */
+        $scope.fetch = function () {
+            $scope.isLoggedIn = (typeof $scope.SYNERGY.session.session_id !== "undefined" && $scope.SYNERGY.session.session_id.length > 1) ? 1 : 0;
+            var action = window.location + "";
+            action = action.substring(action.lastIndexOf("/") + 1);
+            if ($scope.username.length < 1) {
+                if (typeof $scope.SYNERGY.session.session_id !== "undefined" || $scope.SYNERGY.session.session_id.length > 1) {
+                    $scope.username = $scope.SYNERGY.session.username;
+                }
+            }
+            if ($scope.username.length < 1) {
+                return;
+            }
+            userHttp.get($scope, $scope.username, function (data) {
+
+                data.assignments.forEach(function (trun) {
+                    if (trun.projectName === null || trun.projectName === "") {
+                        trun.projectName = $scope.SYNERGY.product;
+                    }
+                });
+                setProject(data.authorOf);
+                setProject(data.ownerOf);
+                setProject(data.favorites);
+                $scope.user = data;
+                if ($scope.username === $scope.SYNERGY.session.username) {
+                    $scope.rights = 1;
+                }
+
+                $scope.$emit("updateBreadcrumbs", {link: "user/" + $scope.username, title: $scope.username});
+            }, $scope.generalHttpFactoryError);
+        };
+
+        function setProject(specifications) {
+            for (var i = 0, max = specifications.length; i < max; i++) {
+                specifications[i]._project = specifications[i].ext.hasOwnProperty("projects") && specifications[i].ext.projects.length > 0 ? specifications[i].ext.projects[0].name : $scope.SYNERGY.product;
+            }
+        }
+
+        $scope.editName = function () {
+            if ($scope.rights === 1) {
+                var invalidPassword = typeof $scope.user.password === "undefined" || $scope.user.password === null || $scope.user.password.length < 1;
+                if ($scope.profileForm.$invalid || ($scope.updatePassword && invalidPassword)) {
+                    $scope.SYNERGY.modal.update("Missing required fields", "");
+                    $scope.SYNERGY.modal.show();
+                    return;
+                }
+
+                var _u = new SynergyModels.User($scope.user.firstName, $scope.user.lastName, $scope.user.username, "", -1);
+                _u.emailNotifications = $scope.user.emailNotifications;
+                _u.email = $scope.user.email;
+                if ($scope.updatePassword) {
+                    _u.password = $scope.user.password;
+                }
+                userHttp.edit($scope, _u, function (data) {
+                    $scope.SYNERGY.logger.log("Done", "updated", "INFO", "alert-success");
+                }, $scope.generalHttpFactoryError);
+            }
+        };
+
+        /**
+         * Removes specification from user's list of favorites
+         * @param {Number} id specification ID
+         */
+        $scope.toggleFavorite = function (id) {
+            var spec = {"id": id, "isFavorite": 0};
+            userHttp.toggleFavorite($scope, spec, function (data) {
+                $scope.SYNERGY.logger.log("Done", "Specification removed from favorites", "INFO", "alert-success");
+                for (var k = 0, max = $scope.user.favorites.length; k < max; k += 1) {
+                    if (parseInt($scope.user.favorites[k].id, 10) === parseInt(id, 10)) {
+                        $scope.user.favorites.splice(k, 1);
+                        return;
+                    }
+                }
+            }, $scope.generalHttpFactoryError);
+        };
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+
+        $scope.uploadFile = function () {
+            new SynergyHandlers.FileUploader([], "dropbox", $scope.SYNERGY.uploadFileLimit, $scope.SYNERGY.server.buildURL("profile_img", {"id": $scope.user.id}), function (title, msg, level, style, fileName, newSrc) {
+                $scope.SYNERGY.logger.log(title, msg, level, style);
+                $scope.user.profileImg = newSrc;
+                try {
+                    if (!$scope.$$phase) {
+                        $scope.$apply();
+                    }
+                } catch (e) {
+                }
+            }, function (title, msg, level, style) {
+                $scope.SYNERGY.logger.log("Action failed", msg, "INFO", "alert-error");
+                try {
+                    if (!$scope.$$phase) {
+                        $scope.$apply();
+                    }
+                } catch (e) {
+                }
+            }).uploadFileFromFileChooser("fileToUpload");
+        };
+
+        $scope.resetFile = function () {
+            userHttp.resetProfileImg($scope, $scope.user.id, function (data) {
+                $scope.user.profileImg = data;
+            }, $scope.generalHttpFactoryError);
+        };
+
+    }
+    /**
+     * @param {LabelFct} labelHttp
+     * @returns {undefined} 
+     */
+    function LabelFilterCtrl($scope, $routeParams, labelHttp) {//authService
+        $scope.$emit("updateNavbar", {item: "nav_home"});
+        $scope.result = {};
+        $scope.label = $routeParams.label || "";
+        $scope.page = $routeParams.page || 1;
+        $scope.next = 0;
+        $scope.prev = 0;
+        $scope.nextPage = 1;
+        $scope.prevPage = 1;
+        /**
+         * Loads data from server
+         */
+        $scope.fetch = function () {
+            if ($scope.label.length > 0) {
+                labelHttp.findCases($scope, $scope.label, $scope.page, function (data) {
+                    $scope.result = data;
+                    $scope.$emit("updateBreadcrumbs", {link: "label/" + $scope.label + "/page/1", title: $scope.label});
+                    $scope.next = (data.nextUrl.length > 1) ? 1 : 0;
+                    $scope.prev = (data.prevUrl.length > 1) ? 1 : 0;
+                    $scope.nextPage = parseInt($scope.page, 10) + 1;
+                    $scope.prevPage = parseInt($scope.page, 10) - 1;
+                }, $scope.generalHttpFactoryError);
+            }
+        };
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+    /**
+     * @param {UsersFct} usersHttp
+     * @param {TribeFct} tribeHttp description
+     * @returns {undefined} 
+     */
+    function TribeCtrl($scope, $location, $routeParams, $timeout, usersHttp, tribeHttp, sanitizerHttp, specificationsHttp, SynergyUtils, SynergyModels) {//authService
+        $scope.$emit("updateNavbar", {item: "nav_home"});
+        $scope.tribe = {};
+        $scope.id = $routeParams.id || -1;
+        $scope.rights = 0;
+        $scope.refreshCodemirror = false;
+        var currentAction = "";
+        var currentId = "";
+        $scope.suggestions = [];
+        $scope.users_suggestions = [];
+        $scope.userToBeAdded = "pepa";
+        $scope.specifications = [];
+        $scope.newSpecification = -1;
+        $scope.users = [];
+        $scope.loadingUsers = false;
+        $scope.toggleMembers = false;
+        /**
+         * Loads data from server
+         */
+        $scope.fetch = function () {
+            var action = window.location + "";
+            action = action.substring(action.lastIndexOf("/") + 1);
+            if ($scope.id > 0) {
+                tribeHttp.get($scope, false, $scope.id, function (data) {
+                    setProject(data.ext);
+                    $scope.tribe = data;
+                    $scope.refreshCodemirror = true;
+                    $scope.$emit("updateBreadcrumbs", {link: "tribe/" + $scope.id, title: data.name});
+                    if ($scope.tribe.controls.length > 0) {
+                        $scope.rights = 1;
+                    }
+                    if (action === "edit") {
+                        loadSpecifications();
+                        loadUsers();
+                    }
+                }, $scope.generalHttpFactoryError);
+            }
+        };
+
+        function setProject(ext) {
+            if (!ext.hasOwnProperty("specifications")) {
+                return;
+            }
+            for (var i = 0, max = ext.specifications.length; i < max; i++) {
+                ext.specifications[i]._project = ext.specifications[i].hasOwnProperty("projects") && ext.specifications[i].projects.length > 0 ? ext.specifications[i].projects[0].name : $scope.SYNERGY.product;
+            }
+        }
+
+        $scope.loadPreview = function () {
+            var _t = "<h1>" + ($scope.tribe.name || "") + "</h1><h3>Description</h3><div class='well'>" + ($scope.tribe.description || "") + "</div>";
+            sanitizerHttp.getSanitizedInput($scope, _t, function (data) {
+                $scope.preview = data;
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Loads all specifications (in edit page so tribe leader can add specification to tribe)
+         */
+        function loadSpecifications() {
+            specificationsHttp.get($scope, "allRaw", function (data) {
+                var d = [];
+                var p;
+                for (var i = 0, max = data.length; i < max; i += 1) {
+                    p = data[i].ext.hasOwnProperty("projects") && data[i].ext.projects.length > 0 ? data[i].ext.projects[0].name : $scope.SYNERGY.product;
+                    d[i] = {
+                        title: data[i].title,
+                        version: data[i].version,
+                        value: data[i].title + " (" + p + " " + data[i].version + ")",
+                        id: data[i].id
+                    };
+                }
+                $scope.specifications = d;
+            }, $scope.generalHttpFactoryError);
+        }
+
+        $scope.addSpecification = function () {
+            var _index = parseInt($scope.newSpecification);
+            for (var i = 0, max = $scope.tribe.ext.specifications.length; i < max; i++) {
+                if (parseInt($scope.tribe.ext.specifications[i].id) === _index) {
+                    $scope.SYNERGY.logger.log("Oops", "Specification already added to tribe", "INFO", "alert-info");
+                    return;
+                }
+            }
+
+            tribeHttp.addSpecification($scope, $scope.id, $scope.newSpecification, function (data, specificationId) {
+                $scope.SYNERGY.logger.log("Done", "Specification added to tribe", "INFO", "alert-success");
+                var matchingIndex = findSpecification(parseInt(specificationId, 10));
+                if (matchingIndex > 0) {
+                    $scope.tribe.ext.specifications.push($scope.specifications[matchingIndex]);
+                }
+            }, $scope.generalHttpFactoryError);
+        };
+
+        $scope.removeSpecification = function (specificationId) {
+            tribeHttp.removeSpecification($scope, $scope.id, specificationId, function (data, specificationId) {
+                $scope.SYNERGY.logger.log("Done", "Specification removed from tribe", "INFO", "alert-success");
+                for (var i = 0, max = $scope.tribe.ext.specifications.length; i < max; i++) {
+                    if (parseInt($scope.tribe.ext.specifications[i].id, 10) === specificationId) {
+                        $scope.tribe.ext.specifications.splice(i, 1);
+                        return;
+                    }
+                }
+            }, $scope.generalHttpFactoryError);
+        };
+
+        function findSpecification(id) {
+            for (var i = 0, max = $scope.specifications.length; i < max; i++) {
+                if ($scope.specifications[i].id === id) {
+                    return i;
+                }
+            }
+            return -1;
+        }
+
+        function loadUsers() {
+            if ($scope.users.length > 0) { // already loaded
+                return;
+            }
+            $scope.loadingUsers = true;
+            usersHttp.getAll($scope, function (data) {
+                if (SynergyUtils.definedNotNull(data) && SynergyUtils.definedNotNull(data.users)) {
+                    for (var i = 0, max = data.users.length; i < max; i++) {
+                        data.users[i].displayName = data.users[i].firstName + " " + data.users[i].lastName + " (" + data.users[i].username + ")";
+                    }
+                }
+                $scope.loadingUsers = false;
+                $scope.users = data.users;
+            }, function (data) {
+                $scope.SYNERGY.logger.log("Action failed", "Unable to load list of users", "INFO", "alert-error");
+                $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+            });
+        }
+        /**
+         * Shows confirmation dialog to remove user from tribe and saves selected username as currentId
+         * @param {String} username
+         */
+        $scope.removeFromTribe = function (username) {
+            $("#deleteModalLabel").text("Remove user from tribe?");
+            $("#deleteModalBody").html("<p>Do you really want to revoke user's membership?</p>");
+            $("#deleteModal").modal("toggle");
+            currentAction = "deleteUser";
+            currentId = username;
+        };
+
+        /**
+         * Removes users from tribe (calls server)
+         * @param {String} username username of users to be removed from tribe
+         */
+        $scope.performRemoveFromTribe = function (username) {
+            $("#deleteModal").modal("toggle");
+            var memberToBeRemoved = username;
+            tribeHttp.revokeMembership($scope, username, $scope.id, function (data) {
+                $scope.SYNERGY.logger.log("Done", "User removed from tribe", "INFO", "alert-success");
+                for (var i = 0, max = $scope.tribe.members.length; i < max; i++) {
+                    if ($scope.tribe.members[i].username === memberToBeRemoved) {
+                        $scope.tribe.members.splice(i, 1);
+                        return;
+                    }
+                }
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Bases od $scope.currentAction it calls some method
+         */
+        $scope.performAction = function () {
+            switch (currentAction) {
+                case "deleteUser":
+                    $scope.performRemoveFromTribe(currentId);
+                    break;
+                default:
+                    break;
+            }
+        };
+
+        /**
+         * Performrs action with current tribe
+         * @param {String} action unless it is "delete", it reroutes to tribe/id/action otherwise shows confirmation dialog
+         */
+        $scope.performTribeAction = function (action) {
+            if (action !== "delete") {
+                $location.path("tribe/" + $scope.id + "/" + action);
+            }
+        };
+
+        $scope.cancel = function () {
+            window.history.back();
+        };
+
+        /**
+         * Saves modifications made to tribe
+         * @returns {unresolved}
+         */
+        $scope.save = function () {
+            if ($scope.myForm.$invalid || $scope.myForm2.$invalid || typeof $scope.tribe.description === "undefined" || $scope.tribe.description.length < 0) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+            var tribe = new SynergyModels.Tribe($scope.tribe.name, $scope.tribe.description, $scope.tribe.leaderUsername, $scope.tribe.id);
+            tribeHttp.edit($scope, tribe, function (data) {
+                $scope.SYNERGY.modal.update("Tribe updated", "");
+                $scope.SYNERGY.modal.show();
+                $location.path("tribe/" + $scope.tribe.id);
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Adds user of given username to tribe
+         */
+        $scope.addUser = function () {
+            tribeHttp.newMembership($scope, {username: $scope.userToBeAdded}, $scope.tribe.id, function (data) {
+                $scope.SYNERGY.logger.log("Done", "User added to tribe", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+        };
+
+        $scope.showAddUserModal = function () {
+            $scope.toggleMembers = !$scope.toggleMembers;
+            loadUsers();
+        };
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+    /**
+     * @param {RunsFct} runsHttp
+     */
+    function RunsCtrl($scope, $routeParams, runsHttp) {
+        $scope.runs = [];
+        $scope.page = $routeParams.page || 1;
+        $scope.next = 0;
+        $scope.prev = 0;
+        $scope.nextPage = 1;
+        $scope.prevPage = 1;
+
+        /**
+         * Retrieves list of test runs
+         */
+        $scope.fetch = function () {
+            $scope.$emit("updateBreadcrumbs", {link: "runs", title: "Test Runs"});
+            $scope.$emit("updateNavbar", {item: "nav_runs"});
+            runsHttp.get($scope, $scope.page, function (data) {
+
+                data.testRuns.forEach(function (trun) {
+                    if (trun.projectName === null || trun.projectName === "") {
+                        trun.projectName = $scope.SYNERGY.product;
+                    }
+                });
+
+                $scope.runs = data;
+                $scope.next = (data.nextUrl.length > 1) ? 1 : 0;
+                $scope.prev = (data.prevUrl.length > 1) ? 1 : 0;
+                $scope.nextPage = parseInt($scope.page, 10) + 1;
+                $scope.prevPage = parseInt($scope.page, 10) - 1;
+            }, $scope.generalHttpFactoryError, true);
+        };
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+    /**
+     * @param {AssignmentFct} assignmentHttp
+     * @returns {undefined} */
+    function AssignmentCtrl($scope, $routeParams, assignmentHttp) {//authService
+        $scope.$emit("updateNavbar", {item: "nav_home"});
+        $scope.assignment = {};
+        $scope.currentCase = {};
+        $scope.currentCaseId = -1;
+        $scope.currentSuiteId = -1;
+        $scope.id = $routeParams.id || -1;
+        $scope.mode = $routeParams.mode || 2;
+        $scope.project = {};
+        $scope.timeLeft = 0;
+        $scope.suiteIndex = 0;
+        $scope.caseIndex = 0;
+        $scope.casesFinished = 0;
+        $scope.newIssue = "";
+        $scope.attachmentBase = $scope.SYNERGY.server.buildURL("attachment", {});
+        var timeFinished = 0;
+        var _time = 0;
+        $scope.failedAttempt = false;
+        var started = 0;
+        var cachedData = {};
+        $scope.cacheDate = {};
+        $scope.comments = [];
+        $scope.allCases = [];
+        $scope.suiteSetupDisplayed = false;
+        $scope.toggleAction = "display";
+        $scope.caseToPrint = {};
+        $scope.errorMsg = "";
+        $scope.somethingCompleted = false;
+        $scope.pauseButtonTitle = "No test case has been completed yet or nothing has changed since resuming testing";
+        /**
+         * Loads data from server
+         */
+        $scope.fetch = function () {
+            if ($scope.id > 0) {
+
+                if (cachedDataExists()) {
+                    $scope.cacheDate = cachedData.date;
+                    $("#attemptModal").modal("toggle");
+                    return;
+                }
+
+                assignmentHttp.getCommentTypes($scope, function (data) {
+                    data.push({"name": "No comment", "id": -1});
+                    $scope.comments = data;
+                }, $scope.generalHttpFactoryError);
+
+                switch ($scope.mode) {
+                    case "1":
+                        assignmentHttp.start($scope, $scope.id, function (data) {
+                            $scope.assignment = data;
+                            setProject(data.specificationData);
+                            getTimeLeft();
+                            collectCases();
+                            $scope.casesFinished = parseInt(100 * (parseInt(data.completed, 10) / parseInt(data.total, 10)), 10) || 0;
+                            parseData(0, 0, 0);
+                            $scope.caseToPrint = $scope.allCases.filter(function (e) {
+                                return e.caseId === $scope.currentCase.caseId && e.suiteId === $scope.currentCase.suiteId;
+                            })[0];
+                        }, $scope.generalHttpFactoryError);
+                        break;
+                    case "2":
+                        assignmentHttp.restart($scope, $scope.id, function (data) {
+                            $scope.assignment = data;
+                            setProject(data.specificationData);
+                            $scope.timeLeft = parseInt($scope.assignment.specificationData.estimation, 10);
+                            collectCases();
+                            $scope.casesFinished = parseInt(100 * (parseInt(data.completed, 10) / parseInt(data.total, 10)), 10) || 0;
+                            parseData(0, 0, 0);
+                            $scope.caseToPrint = $scope.allCases.filter(function (e) {
+                                return e.caseId === $scope.currentCase.caseId && e.suiteId === $scope.currentCase.suiteId;
+                            })[0];
+                        }, $scope.generalHttpFactoryError);
+                        break;
+                    default:
+                        break;
+                }
+
+            }
+        };
+        function setProject(data) {
+            if (data.hasOwnProperty("ext") && data.ext.hasOwnProperty("projects") && data.ext.projects.length > 0) {
+                $scope.project = data.ext.projects[0];
+            } else {
+                $scope.project = {"name": $scope.SYNERGY.product, id: -2};
+            }
+        }
+        /**
+         * Collects all cases to show them in navigation combo box together with their potential progress
+         * @returns {undefined}
+         */
+        function collectCases() {
+            var cases = [];
+            for (var i = 0, max = $scope.assignment.specificationData.testSuites.length; i < max; i += 1) {
+                for (var j = 0, max2 = $scope.assignment.specificationData.testSuites[i].testCases.length; j < max2; j += 1) {
+                    var p = getProgressForCase($scope.assignment.specificationData.testSuites[i].testCases[j].id, $scope.assignment.specificationData.testSuites[i].id);
+                    cases.push({
+                        "name": $scope.assignment.specificationData.testSuites[i].testCases[j].title + " (" + $scope.assignment.specificationData.testSuites[i].title + ")" + ((parseInt(p.finished, 10) === 1) ? " - [" + p.result + "]" : ""),
+                        "caseId": $scope.assignment.specificationData.testSuites[i].testCases[j].id,
+                        "suiteId": $scope.assignment.specificationData.testSuites[i].id,
+                        "progress": p,
+                        "result": p.result
+                    });
+                }
+            }
+            $scope.allCases = cases;
+        }
+
+        /**
+         * Sets common properties and prints case selected in combo box
+         */
+        $scope.traverseCase = function () {
+            var oldSuiteId = $scope.currentSuiteId;
+            $scope.currentCaseId = parseInt($scope.caseToPrint.caseId, 10);
+            $scope.currentSuiteId = parseInt($scope.caseToPrint.suiteId, 10);
+
+            var _s;
+            for (var i = 0, max = $scope.assignment.progress.specification.testSuites.length; i < max; i += 1) {
+                _s = $scope.assignment.progress.specification.testSuites[i];
+                if (parseInt(_s.id, 10) === parseInt($scope.caseToPrint.suiteId, 10)) {
+                    for (var j = 0, max2 = _s.testCases.length; j < max2; j += 1) {
+                        if (parseInt(_s.testCases[j].id, 10) === parseInt($scope.caseToPrint.caseId, 10)) {
+                            $scope.suiteIndex = i;
+                            $scope.caseIndex = j;
+                        }
+                    }
+                }
+            }
+
+            if (oldSuiteId !== $scope.currentSuiteId) {
+                $scope.suiteSetupDisplayed = true;
+                $scope.toggleAction = "hide";
+            }
+
+            printCase($scope.currentCaseId, $scope.currentSuiteId);
+        };
+
+        $scope.toggleSetup = function () {
+            if ($scope.suiteSetupDisplayed) {
+                $scope.toggleAction = "display";
+            } else {
+                $scope.toggleAction = "hide";
+            }
+            $scope.suiteSetupDisplayed = !$scope.suiteSetupDisplayed;
+        };
+
+        /**
+         * Checks cache for possibly not submitted data
+         * @returns {Boolean} true if there are cached data
+         */
+        function cachedDataExists() {
+            cachedData = $scope.SYNERGY.cache.get("assignment_progress_" + $scope.id);
+            return cachedData && cachedData.date && cachedData.progress ? true : false;
+        }
+
+        $scope.sendCached = function (send) {
+            if (send) {
+                $("#attemptModal").modal("toggle");
+                $scope.assignment.progress = cachedData.progress;
+                $scope.sendResults();
+            } else {
+                $scope.SYNERGY.cache.clear("assignment_progress_" + $scope.id);
+                $scope.fetch();
+            }
+        };
+
+        /**
+         * Finds first unfinished test case. There are 2 approaches: iterate over progress and find matching test case or iterate over specification
+         * data and find matching progress. The better is 2nd one because if specification has some new cases thar are not in this progress, it
+         * can be dynamically adjusted.
+         * @param {Number} caseStartIndex
+         * @param {Number} suiteStartIndex
+         * @param {Number} timeOffset
+         */
+        function parseData(caseStartIndex, suiteStartIndex, timeOffset) {
+            var _s;
+            // find 1st not yet tested test case
+            for (var i = suiteStartIndex, max = $scope.assignment.progress.specification.testSuites.length; i < max; i += 1) {
+                _s = $scope.assignment.progress.specification.testSuites[i];
+                for (var j = caseStartIndex, max2 = _s.testCases.length; j < max2; j += 1) {
+                    if (parseInt(_s.testCases[j].finished, 10) === 0) { // find first not finished case
+                        $scope.currentCaseId = parseInt(_s.testCases[j].id, 10);
+                        $scope.currentSuiteId = parseInt(_s.id, 10);
+                        printCase($scope.currentCaseId, $scope.currentSuiteId);
+                        $scope.suiteIndex = i;
+                        $scope.caseIndex = j;
+                        if (i !== suiteStartIndex || (caseStartIndex === 0 && suiteStartIndex === 0)) {
+                            $scope.suiteSetupDisplayed = true;
+                            $scope.toggleAction = "hide";
+                        }
+                        return;
+                    }
+                }
+                caseStartIndex = 0; // to start from beginning in next suite
+            }
+            $scope.timeLeft = 0;
+            $scope.sendResults(); // this happens if all cases are finished (otherwise return in the for statement above is used this code not executed)
+        }
+
+        function getTimeLeft() {
+            var _s;
+            for (var i = 0, max = $scope.assignment.progress.specification.testSuites.length; i < max; i += 1) {
+                _s = $scope.assignment.progress.specification.testSuites[i];
+                for (var j = 0, max2 = _s.testCases.length; j < max2; j += 1) {
+                    if (parseInt(_s.testCases[j].finished, 10) !== 0) {
+                        if (_s.testCases[j].hasOwnProperty("originalDuration")) {
+                            timeFinished += parseInt(Math.round(_s.testCases[j].originalDuration), 10);
+                        } else {
+                            timeFinished += parseInt(Math.round(_s.testCases[j].duration), 10);
+                        }
+                    }
+                }
+
+            }
+            $scope.timeLeft = parseInt($scope.assignment.specificationData.estimation, 10) - timeFinished;
+        }
+
+        /**
+         * Sends results of testing to server
+         */
+        $scope.sendResults = function () {
+            if ($scope.failedAttempt) {
+                $("#deleteModal").modal("toggle");
+            }
+            $scope.showWaitDialog();
+            assignmentHttp.submitResults($scope, $scope.id, $scope.assignment.progress, function (data) {
+                $scope.SYNERGY.modal.update("Results submitted", "Thank you for testing");
+                $scope.SYNERGY.modal.show();
+                $scope.SYNERGY.cache.clear("assignment_progress_" + $scope.id);
+                $scope.assignment = {};
+                $scope.currentCase = {};
+                $scope.currentCaseId = -1;
+                $scope.currentSuiteId = -1;
+                $scope.id = $routeParams.id || -1;
+                $scope.mode = $routeParams.mode || 2;
+                $scope.timeLeft = 0;
+                $scope.suiteIndex = 0;
+                $scope.caseIndex = 0;
+                $scope.casesFinished = 0;
+                $scope.newIssue = "";
+                $scope.failedAttempt = false;
+                window.history.back();
+            }, function (data) {
+                $scope.errorMsg = data || "";
+                $scope.SYNERGY.modal.show();// hide wait dialog
+                if (!$scope.failedAttempt) {
+                    $scope.failedAttempt = true;
+                    $("#deleteModal").modal("toggle");
+                }
+                $scope.SYNERGY.logger.log("Action failed", data, "INFO", "alert-error");
+                $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+            });
+        };
+
+        /**
+         * Procceeds to the next case in testing. Based on result parameter it marks test case as passed, failed or skipped
+         * and if $scope.trackCaseDuration, it sets new duration of the case (not that it sends time to server but server must be
+         *  configured to track case duration as well)
+         * @param {type} result
+         * @returns {unresolved}
+         */
+        $scope.next = function (result) {
+
+            if (parseInt($scope.assignment.completed, 10) === parseInt($scope.assignment.total, 10)) {
+                $scope.SYNERGY.modal.update("Testing finished", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+            var alreadyTested = (parseInt($scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].finished, 10) === 1) ? true : false;
+            switch (result) {
+                case "passed":
+                    if (!alreadyTested) {
+                        $scope.assignment.completed++;
+                        $scope.casesFinished = parseInt(100 * (parseInt($scope.assignment.completed, 10) / parseInt($scope.assignment.total, 10)), 10);
+
+                        if ($scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].hasOwnProperty("originalDuration")) {
+                            timeFinished += parseInt(Math.round($scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].originalDuration), 10);
+                        } else {
+                            timeFinished += parseInt(Math.round($scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].duration), 10);
+                        }
+                        $scope.timeLeft = parseInt($scope.assignment.specificationData.estimation, 10) - timeFinished;
+
+                    }
+
+                    var timeTaken = (new Date().getTime()) - started;
+                    $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].finished = 1;
+                    $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].result = result;
+                    $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].comment = $scope.currentCase.comment;
+                    $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].commentFreeText = ($scope.currentCase.comment !== -1) ? $scope.currentCase.commentFreeText.substr(0, 100) : "";
+
+                    if (!$scope.SYNERGY.trackCaseDuration) {
+                        timeTaken = $scope.currentCase.duration * 60000;
+                    }
+
+                    if ($scope.newIssue.length > 0) {
+                        $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].issue = $scope.newIssue.split(" ");
+                    } else {
+                        $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].issue = "";
+                    }
+                    $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].duration = timeTaken;
+                    $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].originalDuration = $scope.currentCase.duration;
+                    parseData($scope.caseIndex + 1, $scope.suiteIndex, $scope.currentCase.duration);
+                    break;
+                case "failed":
+                    if ($scope.newIssue.length > 0) {
+                        if (!alreadyTested) {
+                            $scope.assignment.completed++;
+                            $scope.casesFinished = parseInt(100 * (parseInt($scope.assignment.completed, 10) / parseInt($scope.assignment.total, 10)), 10);
+
+                            if ($scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].hasOwnProperty("originalDuration")) {
+                                timeFinished += parseInt(Math.round($scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].originalDuration), 10);
+                            } else {
+                                timeFinished += parseInt(Math.round($scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].duration), 10);
+                            }
+                            $scope.timeLeft = parseInt($scope.assignment.specificationData.estimation, 10) - timeFinished;
+
+                        }
+                        var timeTaken = (new Date().getTime()) - started;
+                        if (!$scope.SYNERGY.trackCaseDuration) {
+                            timeTaken = $scope.currentCase.duration * 60000;
+                        }
+
+
+
+                        $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].issue = $scope.newIssue.split(" ");
+                        $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].finished = 1;
+                        $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].result = result;
+                        $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].duration = timeTaken;
+                        $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].originalDuration = $scope.currentCase.duration;
+                        $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].comment = $scope.currentCase.comment;
+                        $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].commentFreeText = ($scope.currentCase.comment !== -1) ? $scope.currentCase.commentFreeText.substr(0, 100) : "";
+                        parseData($scope.caseIndex + 1, $scope.suiteIndex, $scope.currentCase.duration);
+
+                    } else {
+                        $scope.SYNERGY.modal.update("Missing issue number", "");
+                        $scope.SYNERGY.modal.show();
+                        return;
+                    }
+                    break;
+                case "skipped":
+                    if ($scope.currentCase.comment === -1) {
+                        $scope.SYNERGY.modal.update("Missing reason for skipping", "Please select comment using the combo box below Skip button");
+                        $scope.SYNERGY.modal.show();
+                        return;
+                    }
+
+
+
+                    if (!alreadyTested) {
+                        $scope.assignment.completed++;
+                        $scope.casesFinished = parseInt(100 * (parseInt($scope.assignment.completed, 10) / parseInt($scope.assignment.total, 10)), 10);
+                        if ($scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].hasOwnProperty("originalDuration")) {
+                            timeFinished += parseInt(Math.round($scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].originalDuration), 10);
+                        } else {
+                            timeFinished += parseInt(Math.round($scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].duration), 10);
+                        }
+                        $scope.timeLeft = parseInt($scope.assignment.specificationData.estimation, 10) - timeFinished;
+                    }
+                    $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].issue = "";
+                    $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].finished = 1;
+                    $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].result = result;
+                    $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].originalDuration = $scope.currentCase.duration;
+                    $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].comment = $scope.currentCase.comment;
+                    $scope.assignment.progress.specification.testSuites[$scope.suiteIndex].testCases[$scope.caseIndex].commentFreeText = ($scope.currentCase.comment !== -1) ? $scope.currentCase.commentFreeText.substr(0, 100) : "";
+                    parseData($scope.caseIndex + 1, $scope.suiteIndex, $scope.currentCase.duration);
+                    break;
+                default:
+                    break;
+            }
+            $scope.somethingCompleted = true;
+            $scope.pauseButtonTitle = "Save current progress and continue later";
+            $scope.SYNERGY.cache.put("assignment_progress_" + $scope.id, {"date": new Date().toString(), "progress": $scope.assignment.progress});
+            collectCases();
+            $scope.caseToPrint = $scope.allCases.filter(function (e) {
+                return e.caseId === $scope.currentCase.caseId && e.suiteId === $scope.currentCase.suiteId;
+            })[0];
+            $scope.SYNERGY.util.scrollTo("caseTitle");
+        };
+
+        /**
+         * It goes through specification and sets case in suite with suiteId with caseId to current so it is displayed to user.
+         * If caseId is -1, first case in first suite is displayed
+         * @param {Number} caseId
+         * @param {Number} suiteId
+         */
+        function printCase(caseId, suiteId) {
+            $scope.newIssue = "";
+            if (caseId === -1) {
+                for (var i = 0, max = $scope.assignment.specificationData.testSuites.length; i < max; i += 1) {
+                    for (var j = 0, max2 = $scope.assignment.specificationData.testSuites[i].testCases.length; j < max2; j += 1) {
+
+                        $scope.suiteIndex = i;
+                        $scope.caseIndex = j;
+                        started = new Date().getTime();
+                        $scope.currentCase = {
+                            "title": $scope.assignment.specificationData.testSuites[i].testCases[j].title,
+                            "caseId": $scope.assignment.specificationData.testSuites[i].testCases[j].id,
+                            "suiteId": $scope.assignment.specificationData.testSuites[i].id,
+                            "images": $scope.assignment.specificationData.testSuites[i].testCases[j].images,
+                            "duration": parseInt($scope.assignment.specificationData.testSuites[i].testCases[j].duration, 10),
+                            "steps": $scope.assignment.specificationData.testSuites[i].testCases[j].steps,
+                            "suiteTitle": $scope.assignment.specificationData.testSuites[i].title,
+                            "result": $scope.assignment.specificationData.testSuites[i].testCases[j].result,
+                            "issues": $scope.assignment.specificationData.testSuites[i].testCases[j].issues,
+                            "suiteSetup": $scope.assignment.specificationData.testSuites[i].desc,
+                            "product": $scope.assignment.specificationData.testSuites[i].product,
+                            "comment": -1,
+                            "commentFreeText": "",
+                            "progress": {"finished": 0, "id": caseId, "result": "", "duration": 0, "issue": [], "comment": -1, "commentFreeText": ""},
+                            "component": $scope.assignment.specificationData.testSuites[i].component
+                        };
+                        return;
+                    }
+                }
+
+                return;
+            }
+            for (var i = 0, max = $scope.assignment.specificationData.testSuites.length; i < max; i += 1) {
+                if (suiteId === parseInt($scope.assignment.specificationData.testSuites[i].id, 10)) {
+                    for (var j = 0, max2 = $scope.assignment.specificationData.testSuites[i].testCases.length; j < max2; j += 1) {
+                        if (caseId === parseInt($scope.assignment.specificationData.testSuites[i].testCases[j].id, 10)) {
+                            $scope.currentCase = {
+                                "title": $scope.assignment.specificationData.testSuites[i].testCases[j].title,
+                                "caseId": $scope.assignment.specificationData.testSuites[i].testCases[j].id,
+                                "suiteId": $scope.assignment.specificationData.testSuites[i].id,
+                                "images": $scope.assignment.specificationData.testSuites[i].testCases[j].images,
+                                "duration": parseInt($scope.assignment.specificationData.testSuites[i].testCases[j].duration, 10),
+                                "steps": $scope.assignment.specificationData.testSuites[i].testCases[j].steps,
+                                "suiteTitle": $scope.assignment.specificationData.testSuites[i].title,
+                                "result": $scope.assignment.specificationData.testSuites[i].testCases[j].result,
+                                "issues": $scope.assignment.specificationData.testSuites[i].testCases[j].issues,
+                                "suiteSetup": $scope.assignment.specificationData.testSuites[i].desc,
+                                "product": $scope.assignment.specificationData.testSuites[i].product,
+                                "comment": -1,
+                                "commentFreeText": "",
+                                "progress": getProgressForCase(caseId, suiteId),
+                                "component": $scope.assignment.specificationData.testSuites[i].component
+                            };
+                            initValuesFromProgress();
+                            started = new Date().getTime();
+                            return;
+                        }
+                    }
+                }
+            }
+        }
+
+
+        function initValuesFromProgress() {
+            // issues
+            if ($scope.currentCase.progress.issue && $scope.currentCase.progress.issue.length > 0) {
+                $scope.newIssue = $scope.currentCase.progress.issue.join(" ");
+            }
+            // comment
+            if ($scope.currentCase.progress.comment && $scope.currentCase.progress.comment > 0) {
+                $scope.currentCase.comment = $scope.currentCase.progress.comment;
+                $scope.currentCase.commentFreeText = ($scope.currentCase.progress.hasOwnProperty("commentFreeText")) ? $scope.currentCase.progress.commentFreeText : "";
+            }
+        }
+
+        function getProgressForCase(caseId, suiteId) {
+            for (var i = 0, max = $scope.assignment.progress.specification.testSuites.length; i < max; i++) {
+                if (parseInt($scope.assignment.progress.specification.testSuites[i].id, 10) === parseInt(suiteId, 10)) {
+                    for (var j = 0, max2 = $scope.assignment.progress.specification.testSuites[i].testCases.length; j < max2; j++) {
+                        if (parseInt($scope.assignment.progress.specification.testSuites[i].testCases[j].id, 10) === parseInt(caseId, 10)) {
+                            return $scope.assignment.progress.specification.testSuites[i].testCases[j];
+                        }
+                    }
+                }
+            }
+            return {"finished": 0, "id": caseId, "result": "", "duration": 0, "issue": [], "comment": -1};
+        }
+
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+// ADMINISTRATION
+    function AdminHomeCtrl($scope) {
+        if (!$scope.SYNERGY.session.hasAdminRights()) {
+            return;
+        }
+
+    }
+    /**
+     * 
+     * @param {VersionsFct} versionsHttp
+     * @param {VersionFct} versionHttp
+     */
+    function AdminVersionCtrl($scope, versionsHttp, versionHttp, SynergyModels) {
+
+        $scope.versions = [];
+        $scope.versionAffectedId = 0; // when editing version, modal dialog is opened, this is to show version name when typing new name
+        $scope.versionAffected = "";
+        $scope.newname = "";
+
+        $scope.fetch = function () {
+            if (!$scope.SYNERGY.session.hasAdminRights()) {
+                return;
+            }
+            versionsHttp.getAll($scope, function (data) {
+                $scope.versions = data;
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Based on action parameter, it displays some window and sets versionId and version as to be affected by future modifications
+         * @param {String} action
+         * @param {Number} versionId
+         * @param {String} version
+         */
+        $scope.perform = function (action, versionId, version, isObsolete) {
+            switch (action) {
+                case "edit":
+                    $scope.newname = version;
+                    $scope.isObsolete = (parseInt(isObsolete, 10) === 1) ? true : false;
+                    $scope.versionAffectedId = versionId;
+                    $scope.versionAffected = version;
+                    $("#editVersionModal").modal("toggle");
+                    break;
+                case "create":
+                    $scope.newname = "";
+                    $("#createVersionModal").modal("toggle");
+                    break;
+                case "delete":
+                    $scope.versionAffectedId = versionId;
+                    $scope.versionAffected = version;
+                    $("#deleteModal").modal("toggle");
+                    break;
+                default :
+                    break;
+            }
+        };
+
+        /**
+         * Renames version
+         */
+        $scope.rename = function () {
+
+            if ($scope.myForm.$invalid) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+
+            versionHttp.edit($scope, new SynergyModels.Version($scope.newname, $scope.versionAffectedId, $scope.isObsolete), function () {
+                $scope.SYNERGY.logger.log("Done", "Version renamed", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Deletes version
+         */
+        $scope.remove = function () {
+            $("#deleteModal").modal("toggle");
+            versionHttp.remove($scope, $scope.versionAffectedId, function () {
+                $scope.SYNERGY.logger.log("Done", "Version removed", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Creates a new version
+         */
+        $scope.create = function () {
+
+            if ($scope.myForm2.$invalid) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+            versionHttp.create($scope, new SynergyModels.Version($scope.newname, -1, false), function (data) {
+                $scope.SYNERGY.logger.log("Done", "Version created", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+        };
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+    /**
+     * 
+     * @param {type} $scope
+     * @param {PlatformsFct} platformsHttp
+     * @param {PlatformFct} platformHttp
+     * @returns {undefined} */
+    function AdminPlatformsCtrl($scope, platformsHttp, platformHttp, SynergyModels) {
+
+        $scope.platforms = [];
+        $scope.platformAffectedId = 0;
+        $scope.platformAffected = "";
+        $scope.newname = "";
+
+        $scope.fetch = function () {
+            if (!$scope.SYNERGY.session.hasAdminRights()) {
+                return;
+            }
+            platformsHttp.get($scope, function (data) {
+                $scope.platforms = data;
+            }, $scope.generalHttpFactoryError, false);
+        };
+
+        /**
+         * Opens dialog based on action value and saves $scope.platformAffectedId and $scope.platformAffected (platform name and ID to be affected
+         * by future changes)
+         * @param {String} action action name
+         * @param {Number} platformId platform ID
+         * @param {Number} platform platform name
+         */
+        $scope.perform = function (action, platformId, platform, isActive) {
+            switch (action) {
+                case "edit":
+                    $scope.platformAffectedId = platformId;
+                    $scope.platformAffected = platform;
+                    $scope.newname = platform;
+                    $scope.isActive = (parseInt(isActive, 10) === 1) ? true : false;
+                    $("#editPlatformModal").modal("toggle");
+                    break;
+                case "create":
+                    $("#createPlatformModal").modal("toggle");
+                    break;
+                case "delete":
+                    $scope.platformAffectedId = platformId;
+                    $scope.platformAffected = platform;
+                    $("#deleteModal").modal("toggle");
+                    break;
+                default :
+                    break;
+            }
+        };
+
+        /**
+         * Removes platform
+         */
+        $scope.remove = function () {
+            $("#deleteModal").modal("toggle");
+            platformHttp.remove($scope, $scope.platformAffectedId, function (data) {
+                $scope.SYNERGY.logger.log("Done", "Platform removed", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Renames platform
+         */
+        $scope.rename = function () {
+
+            if ($scope.myForm.$invalid) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+
+            platformHttp.edit($scope, new SynergyModels.Platform($scope.newname, $scope.platformAffectedId, $scope.isActive), function (data) {
+                $scope.SYNERGY.logger.log("Done", "Platform renamed", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Creates a new platform
+         * @returns {unresolved}
+         */
+        $scope.create = function () {
+
+            if ($scope.myForm2.$invalid) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+
+            platformHttp.create($scope, new SynergyModels.Platform($scope.newname, -1), function (data) {
+                $scope.SYNERGY.logger.log("Done", "Platform created", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+        };
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+    /**
+     * @param {UsersFct} usersHttp
+     * @param {TribeFct} tribeHttp
+     * @param {TribesFct} tribesHttp
+     * @returns {undefined} */
+    function AdminTribesCtrl($scope, $location, $timeout, usersHttp, tribeHttp, tribesHttp, sanitizerHttp, SynergyUtils, SynergyModels) {
+
+        $scope.tribes = [];
+        $scope.tribe = {}; // for new tribe page
+        $scope.tribeAffectedId = 0;
+        $scope.tribeAffected = "";
+        $scope.newname = "";
+        $scope.users = [];
+        $scope.importTribesUrl = "http://" + window.location.host + "/dashboard/web/a_tribes.php?export=true";
+
+        $scope.fetch = function () {
+            if (!$scope.SYNERGY.session.hasAdminRights()) {
+                return;
+            }
+            loadUsers();
+            tribesHttp.get($scope, function (data) {
+                $scope.tribes = data;
+            }, $scope.generalHttpFactoryError);
+        };
+
+
+        $scope.importTribes = function () {
+            tribesHttp.importTribes($scope, $scope.importTribesUrl, function (data) {
+                $scope.SYNERGY.logger.log("Done", data + " tribes imported", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+        };
+
+
+        /**
+         * Removes tribe
+         */
+        $scope.deleteTribe = function () {
+            $("#deleteModal").modal("toggle");
+            tribeHttp.remove($scope, $scope.tribeAffectedId, function (data) {
+                $scope.SYNERGY.logger.log("Done", "Tribe removed", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Redirects to URL based on action value
+         * @param {String} action action to be done
+         * @param {String} id Tribe ID
+         * @param {String} tribe Tribe name
+         */
+        $scope.perform = function (action, id, tribe) {
+            switch (action) {
+                case "create":
+                    $location.path("administration/tribes/" + action);
+                    break;
+                default :
+                    break;
+            }
+        };
+
+        /**
+         * Shows delete confirmation dialog
+         * @param {Number} id tribe ID to be removed
+         * @param {String} tribe tribe name
+         */
+        $scope.deleteModal = function (id, tribe) {
+            $scope.tribeAffectedId = id;
+            $scope.tribeAffected = tribe;
+            $("#deleteModal").modal("toggle");
+        };
+
+        function loadUsers() {
+            usersHttp.getAll($scope, function (data) {
+                if (SynergyUtils.definedNotNull(data) && SynergyUtils.definedNotNull(data.users)) {
+                    for (var i = 0, max = data.users.length; i < max; i++) {
+                        data.users[i].displayName = data.users[i].firstName + " " + data.users[i].lastName + " (" + data.users[i].username + ")";
+                    }
+                }
+
+                $scope.users = data.users;
+                if (SynergyUtils.definedNotNull(data) && data.users.length > 0) {
+                    $scope.tribe.leaderUsername = data.users[0].displayName;
+                }
+            }, $scope.generalHttpFactoryError);
+        }
+
+        $scope.loadPreview = function () {
+            var _t = "<h1>" + ($scope.tribe.name || "") + "</h1><h3>Description</h3><div class='well'>" + ($scope.tribe.description || "") + "</div>";
+            sanitizerHttp.getSanitizedInput($scope, _t, function (data) {
+                $scope.preview = data;
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Creates a new tribe
+         */
+        $scope.create = function () {
+
+            if ($scope.myForm2.$invalid || $scope.myForm.$invalid) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+            var tribe = new SynergyModels.Tribe($scope.tribe.name, $scope.tribe.description, ($scope.tribe.leaderUsername), -1);
+            tribeHttp.create($scope, tribe, function (data) {
+                $scope.SYNERGY.modal.update("Tribe created", "");
+                $scope.SYNERGY.modal.show();
+                window.history.back();
+            }, $scope.generalHttpFactoryError);
+        };
+
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+    /**
+     * @param {RunsFct} runsHttp
+     * @param {RunFct} runHttp
+     */
+    function AdminRunsCtrl($scope, $location, $routeParams, runsHttp, runHttp) {
+        $scope.runs = [];
+        $scope.page = $routeParams.page || 1;
+        $scope.orderProp = "title";
+        $scope.next = 0;
+        $scope.prev = 0;
+        $scope.nextPage = 1;
+        $scope.prevPage = 1;
+        var currentActionId = -1;
+        var currentAction = "";
+
+        $scope.fetch = function () {
+            if (!$scope.SYNERGY.session.hasAdminRights()) {
+                return;
+            }
+            runsHttp.get($scope, $scope.page, function (data) {
+
+                data.testRuns.forEach(function (trun) {
+                    if (trun.projectName === null || trun.projectName === "") {
+                        trun.projectName = $scope.SYNERGY.product;
+                    }
+                });
+
+                $scope.runs = data;
+                $scope.next = (data.nextUrl.length > 1) ? 1 : 0;
+                $scope.prev = (data.prevUrl.length > 1) ? 1 : 0;
+                $scope.nextPage = parseInt($scope.page, 10) + 1;
+                $scope.prevPage = parseInt($scope.page, 10) - 1;
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Unless action is delete, it redirects to action URL, otherwise opens confirmation dialog
+         * @param {String} action action
+         * @param {Number} id run ID
+         * @param {String} run run name
+         */
+        $scope.perform = function (action, id, run) {
+            switch (action) {
+                case "delete":
+                    $("#deleteModalLabel").text("Delete test run?");
+                    $("#deleteModalBody").html("<p>This action will also delete all test assignments for this test run. Do you want to continue?</p>");
+                    $("#deleteModal").modal("toggle");
+                    currentActionId = id;
+                    currentAction = "delete";
+                    break;
+                case "notify":
+                    $("#deleteModalLabel").text("Send notifications?");
+                    $("#deleteModalBody").html("<p>Do you really want to send email notifications to testers with incomplete test assignment?</p>");
+                    $("#deleteModal").modal("toggle");
+                    currentAction = "notify";
+                    currentActionId = id;
+                    break;
+                case "freeze":
+                    var _run = $scope.runs.testRuns.filter(function (e) {
+                        return e.id === id;
+                    })[0];
+
+                    var target = (_run.isActive ? 0 : 1);
+                    runHttp.freezeRun($scope, id, target, function (data) {
+                        _run = $scope.runs.testRuns.filter(function (e) {
+                            return e.id === id;
+                        })[0];
+                        _run.isActive = target;
+                        $scope.SYNERGY.logger.log("Done", "Test run " + (target === 1 ? "unfrozen" : "frozen"), "INFO", "alert-success");
+                    }, $scope.generalHttpFactoryError);
+                    break;
+                default:
+                    $location.path("administration/run/" + id + "/" + action);
+                    break;
+            }
+        };
+
+        $scope.performAction = function () {
+            switch (currentAction) {
+                case "delete":
+                    remove();
+                    break;
+                case "notify":
+                    $("#deleteModal").modal("toggle");
+                    runHttp.sendNotifications($scope, currentActionId, function (data) {
+                        $scope.SYNERGY.logger.log("Done", data, "INFO", "alert-success");
+                    }, function (data) {
+                        $scope.SYNERGY.logger.log("Action failed", "", "INFO", "alert-error");
+                        $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+                    });
+                    break;
+                default:
+                    break;
+            }
+        };
+
+        /**
+         * Removes test run
+         */
+        function remove() {
+            $("#deleteModal").modal("toggle");
+            runHttp.remove($scope, currentActionId, function (data) {
+                $scope.SYNERGY.logger.log("Done", "Test run removed", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+        }
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+    /**
+     * @param {RunFct} runHttp
+     */
+    function AdminRunCtrl($scope, $routeParams, runHttp, $location, attachmentHttp, sanitizerHttp, projectsHttp, SynergyModels, SynergyHandlers) {
+        $scope.attachmentBase = $scope.SYNERGY.server.buildURL("run_attachment", {});
+        $scope.refreshCodemirror = false;
+        $scope.projects = [];
+        $scope.testRun = new SynergyModels.TestRun("", "", "", "", "");
+        $scope.id = $routeParams.id || -1;
+        var currentAction = "";
+        var currentActionId = -1;
+
+        try {
+            $("#start").datetimepicker({dateFormat: "yy-mm-dd", timeFormat: "HH:mm:ss"});
+            $("#end").datetimepicker({dateFormat: "yy-mm-dd", timeFormat: "HH:mm:ss"});
+        } catch (e) {
+        }
+        $scope.loadPreview = function () {
+            var _t = "<h1>" + ($scope.testRun.title || "") + "</h1><h3>Description</h3><div class='well'>" + ($scope.testRun.desc || "") + "</div>";
+            sanitizerHttp.getSanitizedInput($scope, _t, function (data) {
+                $scope.preview = data;
+            }, $scope.generalHttpFactoryError);
+        };
+        /**
+         * Starts with action on given run attachment
+         * Otherwise confirmation dialog is opened
+         * @param {String} action action name
+         * @param {Number} id attachment ID
+         */
+        $scope.performAttachment = function (action, id) {
+            switch (action) {
+                case "delete":
+                    $("#deleteModalLabel").text("Delete attachment?");
+                    $("#deleteModalBody").html("<p>Do you really want to delete attachment?</p>");
+                    $("#deleteModal").modal("toggle");
+                    currentAction = "deleteAttachment";
+                    currentActionId = id;
+                    break;
+                default:
+                    break;
+            }
+        };
+
+        $scope.performAction = function () {
+            switch (currentAction) {
+                case "deleteAttachment":
+                    $("#deleteModal").modal("toggle");
+                    if (typeof $scope.SYNERGY.session.session_id === "undefined" || $scope.SYNERGY.session.session_id.length < 1) {
+                        return;
+                    }
+                    attachmentHttp.removeRunAttachment($scope, currentActionId, function (data) {
+                        $scope.SYNERGY.logger.log("Done", "Attachment deleted", "INFO", "alert-success");
+                        $scope.fetch();
+                    }, function (data) {
+                        $scope.SYNERGY.logger.log("Action failed", "", "INFO", "alert-error");
+                        $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+                        $scope.fetch();
+                    });
+                    break;
+                default:
+                    break;
+            }
+        };
+
+        $scope.cancel = function () {
+            window.history.back();
+        };
+
+        /**
+         * Creates a new test run
+         */
+        $scope.create = function () {
+            var start = $("#start").val();
+            var stop = $("#end").val();
+            if ($scope.myForm.$invalid || start.length < 1 || stop.length < 1) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+            var _run = new SynergyModels.TestRun($scope.testRun.title, $scope.testRun.desc, $scope.getUTCTime(start), $scope.getUTCTime(stop), $scope.id).setNotifications($scope.testRun.notifications);
+            _run.projectId = $scope.testRun.projectId;
+            runHttp.create($scope, _run, function (data) {
+                $scope.SYNERGY.modal.update("Test run created", "");
+                $scope.SYNERGY.modal.show();
+                $location.path("administration/runs/page/1");
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Modifies test run
+         */
+        $scope.edit = function () {
+            var start = $("#start").val();
+            var stop = $("#end").val();
+            if ($scope.myForm.$invalid || start.length < 1 || stop.length < 1) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+            var _run = new SynergyModels.TestRun($scope.testRun.title, $scope.testRun.desc, $scope.getUTCTime(start), $scope.getUTCTime(stop), $scope.id).setNotifications($scope.testRun.notifications);
+            _run.projectId = $scope.testRun.projectId;
+            runHttp.edit($scope, _run, function (data) {
+                $scope.SYNERGY.modal.update("Test run updated", "");
+                $scope.SYNERGY.modal.show();
+                window.history.back();
+            }, $scope.generalHttpFactoryError);
+        };
+
+        $scope.uploadFile = function () {
+            new SynergyHandlers.FileUploader([], "dropbox", $scope.SYNERGY.uploadFileLimit, $scope.SYNERGY.server.buildURL("run_attachment", {"id": $scope.id}), function (title, msg, level, style, fileName) {
+                $scope.SYNERGY.logger.log(title, msg, level, style);
+                $scope.fileName = fileName;
+                $scope.fetch();
+            }, function (title, msg, level, style) {
+                $scope.SYNERGY.logger.log(title, msg, level, style);
+            }).uploadFileFromFileChooser("fileToUpload");
+        };
+
+        $scope.validDates = function () {
+            $scope.testRun.start = $("#start").val();
+            $scope.testRun.stop = $("#end").val();
+            return ($scope.testRun.start.length < 1 || $scope.testRun.stop.length < 1) ? false : true;
+        };
+
+        $scope.fetch = function () {
+            if (!$scope.SYNERGY.session.hasAdminRights()) {
+                return;
+            }
+            projectsHttp.getAll($scope, function (data) {
+                $scope.projects = data;
+                if ($scope.testRun.projectName === null && $scope.projects.length > 0) {
+                    $scope.testRun.projectId = $scope.projects[0].id;
+                }
+            }, $scope.generalHttpFactoryError);
+            var action = window.location + "";
+            action = action.substring(action.lastIndexOf("/") + 1);
+            switch (action) {
+                case "edit":
+                    if ($scope.id < 0) {
+                        return;
+                    }
+                    runHttp.getOverview($scope, false, $scope.id, function (data) {
+                        $scope.testRun = data;
+                        $scope.testRun.start = $scope.getLocalDateTime($scope.testRun.start);
+                        $scope.testRun.end = $scope.getLocalDateTime($scope.testRun.end);
+                        if ($scope.projects.length > 0 && data.projectName === null) {
+                            $scope.testRun.projectId = $scope.projects[0].id;
+                        }
+                        $scope.refreshCodemirror = true;
+                    }, $scope.generalHttpFactoryError);
+                    break;
+                default:
+                    break;
+            }
+        };
+
+        new SynergyHandlers.FileUploader([], "dropbox", $scope.SYNERGY.uploadFileLimit, $scope.SYNERGY.server.buildURL("run_attachment", {"id": $scope.id}), function (title, msg, level, style, fileName) {
+            $scope.SYNERGY.logger.log(title, msg, level, style);
+            $scope.fileName = fileName;
+            $scope.fetch();
+        }, function (title, msg, level, style) {
+            $scope.SYNERGY.logger.log(title, msg, level, style);
+        });
+
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+    /**
+     * 
+     * @param {type} $scope
+     * @param {type} $routeParams
+     * @param {AssignmentsFct} assignmentsHttp
+     * @param {UsersFct} usersHttp
+     * @param {LabelsFct} labelsHttp
+     * @param {PlatformsFct} platformsHttp
+     * @param {TribesFct} tribesHttp
+     * @param {VersionsFct} versionsHttp
+     * @returns {undefined}
+     */
+    function AdminMatrixAssignmentCtrl($scope, $routeParams, assignmentsHttp, usersHttp, labelsHttp, platformsHttp, tribesHttp, versionsHttp, SynergyUtils) {
+        $scope.assignment = {platforms: [], users: [], tribes: [], runId: $routeParams.id};
+        $scope.selectedPlatforms = [];
+        $scope.platforms = [];
+        $scope.platformId = -1;
+        $scope.ready = 0; // if < 5, page shows "please wait" message 
+
+        $scope.fetch = function () {
+            // load all required data
+            loadPlatforms();
+            loadLabels();
+            loadUsers();
+            loadVersions();
+            tribesHttp.get($scope, function (data) {
+                $scope.tribes = data;
+                $scope.ready++;
+            }, $scope.generalHttpFactoryError);
+        };
+
+        function loadVersions() {
+            versionsHttp.get($scope, false, function (data) {
+                $scope.versions = data;
+                $scope.ready++;
+            }, $scope.generalHttpFactoryError);
+        }
+
+        function loadPlatforms() {
+            platformsHttp.get($scope, function (data) {
+                $scope.platforms = data;
+                $scope.ready++;
+            }, $scope.generalHttpFactoryError, false);
+        }
+
+        function loadUsers() {
+            usersHttp.getAll($scope, function (data) {
+                if (SynergyUtils.definedNotNull(data) && SynergyUtils.definedNotNull(data.users)) {
+                    for (var i = 0, max = data.users.length; i < max; i++) {
+                        data.users[i].displayName = data.users[i].firstName + " " + data.users[i].lastName + " (" + data.users[i].username + ")";
+                    }
+                }
+                $scope.ready++;
+                $scope.users = data.users;
+            }, $scope.generalHttpFactoryError);
+        }
+
+        function loadLabels() {
+            labelsHttp.getAll($scope, function (data) {
+                $scope.ready++;
+                data.push({"label": "None", "id": "-1"});
+                $scope.labels = data;
+            }, $scope.generalHttpFactoryError);
+        }
+
+        $scope.create = function () {
+            if (($scope.assignment.users.length < 1 && $scope.assignment.tribes.length < 1) || $scope.platforms.length < 1) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+            $scope.showWaitDialog();
+            assignmentsHttp.create($scope, $scope.assignment, function (data) {
+                $scope.SYNERGY.modal.update("Assignment created", "");
+                $scope.SYNERGY.modal.show();
+                window.history.back();
+            }, function (data, status) {
+                $scope.SYNERGY.modal.show();
+                $scope.SYNERGY.logger.log("Action failed for users ", data, "INFO", "alert-error");
+                $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+            });
+        };
+        /**
+         * Adds platform to selected
+         */
+        $scope.addSelectedPlatform = function () {
+            if (!$scope.platformId || platformAlreadySelected(parseInt($scope.platformId, 10))) { // avoid on load displaying empty 
+                return;
+            }
+            $scope.assignment.platforms.push({id: parseInt($scope.platformId, 10), name: getPlatformName(parseInt($scope.platformId, 10))});
+            $scope.platformId = -1;
+        };
+
+        $scope.addSelectedTribe = function () {
+            if (!$scope.tribeId || tribeAlreadySelected(parseInt($scope.tribeId, 10))) { // avoid on load displaying empty 
+                return;
+            }
+            $scope.assignment.tribes.push({id: parseInt($scope.tribeId, 10), name: getTribeName(parseInt($scope.tribeId, 10))});
+            $scope.tribeId = -1;
+        };
+
+        $scope.addSelectedUser = function () {
+            if (!$scope.username || userAlreadySelected($scope.username)) { // avoid on load displaying empty 
+                return;
+            }
+            $scope.assignment.users.push({username: $scope.username, displayName: getDisplayName($scope.username)});
+            $scope.username = "";
+        };
+
+        function userAlreadySelected(username) {
+            for (var i = 0, max = $scope.assignment.users.length; i < max; i++) {
+                if (username === $scope.assignment.users[i].username) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        function tribeAlreadySelected(tribeID) {
+            for (var i = 0, max = $scope.assignment.tribes.length; i < max; i++) {
+                if (tribeID === $scope.assignment.tribes[i].id) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        function platformAlreadySelected(platformID) {
+            for (var i = 0, max = $scope.assignment.platforms.length; i < max; i++) {
+                if (platformID === $scope.assignment.platforms[i].id) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /**
+         * removes platform from selected
+         * @param {Number} platformID
+         */
+        $scope.removeSelectedPlatform = function (platformID) {
+            for (var i = 0, max = $scope.assignment.platforms.length; i < max; i++) {
+                if (platformID === $scope.assignment.platforms[i].id) {
+                    $scope.assignment.platforms.splice(i, 1);
+                    $scope.platformId = -1;
+                    return;
+                }
+            }
+        };
+
+        $scope.removeSelectedTribe = function (tribeId) {
+            for (var i = 0, max = $scope.assignment.tribes.length; i < max; i++) {
+                if (tribeId === $scope.assignment.tribes[i].id) {
+                    $scope.assignment.tribes.splice(i, 1);
+                    $scope.tribesId = -1;
+                    return;
+                }
+            }
+        };
+
+        $scope.removeSelectedUser = function (username) {
+            for (var i = 0, max = $scope.assignment.users.length; i < max; i++) {
+                if (username === $scope.assignment.users[i].username) {
+                    $scope.assignment.users.splice(i, 1);
+                    $scope.username = -1;
+                    return;
+                }
+            }
+        };
+        function getDisplayName(username) {
+            for (var i = 0, max = $scope.users.length; i < max; i++) {
+                if ($scope.users[i].username === username) {
+                    return $scope.users[i].displayName;
+                }
+            }
+            return "oops";
+        }
+
+        function getTribeName(tribeID) {
+            for (var i = 0, max = $scope.tribes.length; i < max; i++) {
+                if ($scope.tribes[i].id === tribeID) {
+                    return $scope.tribes[i].name;
+                }
+            }
+            return "oops";
+        }
+
+        function getPlatformName(platformID) {
+            for (var i = 0, max = $scope.platforms.length; i < max; i++) {
+                if ($scope.platforms[i].id === platformID) {
+                    return $scope.platforms[i].name;
+                }
+            }
+            return "oops";
+        }
+
+        $scope.cancel = function () {
+            window.history.back();
+        };
+
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+    /**
+     * 
+     * @param {SpecificationsFct} specificationsHttp
+     * @param {AssignmentFct} assignmentHttp
+     * @param {UsersFct} usersHttp
+     * @param {LabelsFct} labelsHttp
+     * @param {PlatformsFct} platformsHttp description
+     * @param {TribesFct} tribesHttp description
+     * @returns {undefined} */
+    function AdminAssignmentCtrl($scope, $timeout, $routeParams, specificationsHttp, assignmentsHttp, usersHttp, labelsHttp, platformsHttp, tribesHttp, versionsHttp, assignmentHttp, runHttp, SynergyUtils, SynergyModels) {
+        $scope.testRunId = $routeParams.id;
+        $scope.assignments = [];
+        $scope.platforms = [];
+        $scope.users = [];
+        $scope.ready = 0; // if < 4, message is displayed
+        $scope.value = {};
+
+        $scope.fetch = function () {
+            if (!$scope.SYNERGY.session.hasAdminRights()) {
+                return;
+            }
+            loadPlatforms();
+            loadUsers();
+            loadLabels();
+            loadSpecifications();
+        };
+
+        function loadPlatforms() {
+            platformsHttp.get($scope, function (data) {
+                $scope.platforms = data;
+                $scope.ready++;
+                if (SynergyUtils.definedNotNull(data) && data.length > 0) {
+                    $scope.value.platform = data[0];
+                }
+            }, $scope.generalHttpFactoryError, false);
+        }
+
+        function loadLabels() {
+            labelsHttp.getAll($scope, function (data) {
+                $scope.ready++;
+                data.push({"label": "None", "id": "-1"});
+                $scope.labels = data;
+                $scope.value.label = data[data.length - 1];
+            }, $scope.generalHttpFactoryError);
+        }
+
+        function loadUsers() {
+            usersHttp.getAll($scope, function (data) {
+                if (SynergyUtils.definedNotNull(data) && data.users) {
+                    for (var i = 0, max = data.users.length; i < max; i++) {
+                        data.users[i].displayName = data.users[i].firstName + " " + data.users[i].lastName + " (" + data.users[i].username + ")";
+                    }
+                }
+                $scope.ready++;
+                $scope.users = data.users;
+                if (SynergyUtils.definedNotNull(data) && data.users.length > 0) {
+                    $scope.value.user = $scope.users[0];
+                }
+            }, $scope.generalHttpFactoryError);
+        }
+
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+
+        /**
+         * Adds assignment to the assignments array that will be sent to server
+         */
+        $scope.addAssignment = function () {
+            if (!$scope.value.specification) {
+                $scope.SYNERGY.logger.showMsg("Oops", "No specification selected (maybe tribe does not have any specification?)", "alert-error");
+                return;
+            }
+            var assignment = new SynergyModels.TestAssignment(parseInt($scope.value.platform.id, 10), $scope.value.user.username, parseInt($scope.value.label.id, 10));
+            assignment.specificationId = parseInt($scope.value.specification.id, 10);
+            assignment.testRunId = parseInt($scope.testRunId, 10);
+            assignment.display = {
+                user: $scope.value.user.firstName + " " + $scope.value.user.lastName,
+                label: $scope.value.label.label,
+                platform: $scope.value.platform.name,
+                duplicates: false,
+                specification: $scope.value.specification.value
+            };
+            $scope.assignments.push(assignment);
+            assignmentHttp.checkExists($scope, assignment).then(function (data) {
+                assignment.display.duplicates = true;
+            }, function (response) {
+                switch (response.status) {
+                    case 404:
+                        assignment.display.duplicates = false;
+                        break;
+                    default:
+                        $scope.SYNERGY.logger.log("Action failed", "Unable to check for duplicates", "INFO", "alert-error");
+                        break;
+                }
+            });
+        };
+
+        $scope.removeAssignment = function (index) {
+            $scope.assignments.splice(index, 1);
+        };
+
+        function loadSpecifications() {
+            runHttp.getSpecifications($scope, $scope.testRunId, function (data) {
+                $scope.specifications = [];
+                for (var i = 0, max = data.specifications.length; i < max; i += 1) {
+                    $scope.specifications[i] = {
+                        value: data.specifications[i].title + " (" + ((data.projectName === null || typeof data.projectName === "undefined") ? $scope.SYNERGY.product : data.projectName) + " " + data.specifications[i].version + ")",
+                        info: "<a href='#/specification/" + data.specifications[i].id + "'>view</a>",
+                        id: data.specifications[i].id,
+                        type: "specification",
+                        version: data.specifications[i].version
+                    };
+                }
+                $scope.value.specification = $scope.specifications[0];
+                $scope.ready++;
+            }, $scope.generalHttpFactoryError);
+        }
+
+        $scope.cancel = function () {
+            window.history.back();
+        };
+
+        /**
+         * Creates a new test assignment
+         * @returns {unresolved}
+         */
+        $scope.create = function () {
+
+            $scope.showWaitDialog();
+            assignmentsHttp.createForUsers($scope, $scope.assignments, function (data) {
+                $scope.SYNERGY.modal.update("Assignments created", "");
+                $scope.SYNERGY.modal.show();
+                window.history.back();
+            }, function (data) {
+                $scope.SYNERGY.modal.show();
+                $scope.SYNERGY.logger.log("Action failed ", data, "INFO", "alert-error");
+                $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+            });
+        };
+    }
+    /**
+     * @param {UserFct} userHttp
+     * @param {UsersFct} usersHttp
+     * @returns {undefined} 
+     */
+    function AdminUsersCtrl($scope, $location, $routeParams, userHttp, usersHttp) {
+        $scope.users = [];
+        $scope.page = $routeParams.page || 1;
+        var currentUser = "";
+        $scope.importUsersUrl = "http://" + window.location.host + "/dashboard/web/a_netcatusers.php";
+        $scope.fetch = function () {
+            if (!$scope.SYNERGY.session.hasAdminRights()) {
+                return;
+            }
+            usersHttp.get($scope, $scope.page, function (data) {
+                $scope.users = data.users;
+                $scope.next = (data.nextUrl.length > 1) ? 1 : 0;
+                $scope.prev = (data.prevUrl.length > 1) ? 1 : 0;
+                $scope.nextPage = parseInt($scope.page, 10) + 1;
+                $scope.prevPage = parseInt($scope.page, 10) - 1;
+            }, $scope.generalHttpFactoryError);
+        };
+
+        $scope.importUsers = function () {
+            usersHttp.importUsers($scope, $scope.importUsersUrl, function (data) {
+                $scope.SYNERGY.logger.log("Done", data + " users imported", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+        };
+
+        $scope.retireUsers = function () {
+            usersHttp.retireUsersWithRole($scope, "tester", function () {
+                $scope.SYNERGY.logger.log("Done", "Removed users with role 'tester' retired", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+        };
+
+        $scope.perform = function (action, username) {
+            if (action !== "delete") {
+                $location.path("administration/user/" + username + "/" + action);
+            } else {
+                switch (action) {
+                    case "delete":
+                        $("#deleteModal").modal("toggle");
+                        currentUser = username;
+                        break;
+                    default:
+                        break;
+                }
+            }
+        };
+
+        $scope.newuser = function () {
+            $location.path("administration/user/-1/create");
+        };
+
+        /**
+         * Deletes user
+         */
+        $scope.deleteUser = function () {
+            $("#deleteModal").modal("toggle");
+            userHttp.remove($scope, currentUser, function (data) {
+                $scope.SYNERGY.logger.log("Done", "User removed", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+        };
+
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+    /**
+     * 
+     * @param {UserFct} userHttp
+     * @returns {undefined} 
+     **/
+    function AdminUserCtrl($scope, $routeParams, userHttp, SynergyModels) {
+        $scope.user = {
+        };
+        $scope.username = $routeParams.username || "";
+        $scope.passwordChangeAllowed = !$scope.SYNERGY.useSSO;
+        $scope.updatePassword = false;
+        this.oldUsername = ""; // in case admin changes username, need to track of the old one
+        var self = this;
+        $scope.fetch = function () {
+            if (!$scope.SYNERGY.session.hasAdminRights()) {
+                return;
+            }
+            if ($scope.username === "-1") {
+                // new user
+                return;
+            }
+            userHttp.get($scope, $scope.username, function (data) {
+                self.oldUsername = $scope.username;
+                $scope.user = data;
+            }, $scope.generalHttpFactoryError);
+        };
+
+        $scope.cancel = function () {
+            window.history.back();
+        };
+
+        /**
+         * Creates user
+         */
+        $scope.save = function () {
+            var invalidPassword = typeof $scope.user.password === "undefined" || $scope.user.password === null || $scope.user.password.length < 1;
+            if ($scope.myForm.$invalid || ($scope.updatePassword && invalidPassword)) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+            var u = new SynergyModels.User($scope.user.firstName, $scope.user.lastName, $scope.user.username, $scope.user.role, -1);
+            u.email = $scope.user.email;
+            if ($scope.updatePassword) {
+                u.password = $scope.user.password;
+            }
+            u.emailNotifications = $scope.user.emailNotifications;
+            userHttp.create($scope, u, function (data) {
+                $scope.SYNERGY.modal.update("User created", "");
+                $scope.SYNERGY.modal.show();
+                window.history.back();
+            }, $scope.generalHttpFactoryError);
+        };
+
+        /**
+         * Updates user
+         * @returns {unresolved}
+         */
+        $scope.edit = function () {
+            var invalidPassword = typeof $scope.user.password === "undefined" || $scope.user.password === null || $scope.user.password.length < 1;
+            if ($scope.myForm.$invalid || ($scope.updatePassword && invalidPassword)) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+            var u = new SynergyModels.User($scope.user.firstName, $scope.user.lastName, $scope.user.username, $scope.user.role, -1, self.oldUsername);
+            u.email = $scope.user.email;
+            if ($scope.updatePassword) {
+                u.password = $scope.user.password;
+            }
+            u.emailNotifications = $scope.user.emailNotifications;
+            userHttp.edit($scope, u, function (data) {
+                $scope.SYNERGY.modal.update("User updated", "");
+                $scope.SYNERGY.modal.show();
+                window.history.back();
+            }, $scope.generalHttpFactoryError);
+        };
+
+        var self2 = $scope;
+        $scope.init(function () {
+            self2.fetch();
+        });
+    }
+    /**
+     * 
+     * @param {SettingsFct} settingsHttp
+     * @returns {undefined} 
+     */
+    function AdminSettingCtrl($scope, settingsHttp) {
+        $scope.settings = [];
+        $scope.newValue = "";
+        $scope.newDesc = "";
+        $scope.newKey = "";
+
+        $scope.fetch = function () {
+            if (!$scope.SYNERGY.session.hasAdminRights()) {
+                return;
+            }
+            settingsHttp.get($scope, function (data) {
+                $scope.settings = data;
+            }, $scope.generalHttpFactoryError);
+        };
+
+        $scope.save = function () {
+            for (var i = 0, limit = $scope.settings.length; i < limit; i += 1) {
+                if ($scope.settings[i].value.length < 1) {
+                    $scope.SYNERGY.logger.log("Missing value: ", $scope.settings[i].key, "INFO", "alert-error");
+                    return;
+                }
+            }
+            settingsHttp.edit($scope, $scope.settings, function (data) {
+                $scope.SYNERGY.logger.log("Done", "Settings updated", "INFO", "alert-success");
+                $scope.fetch();
+            }, $scope.generalHttpFactoryError);
+        };
+
+        $scope.addSetting = function () {
+            $scope.settings.push({"value": $scope.newValue, "label": $scope.newDesc, "key": $scope.newKey});
+            $scope.newValue = $scope.newDesc = $scope.newKey = "";
+        };
+
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+    /**
+     * 
+     * @param {AboutFct} aboutHttp
+     * @returns {undefined} 
+     */
+    function AboutCtrl($scope, aboutHttp) {
+        $scope.$emit("updateNavbar", {item: "nav_empty"});
+        $scope.statistics = [];
+
+        $scope.fetch = function () {
+            aboutHttp.get($scope, function (data) {
+                $scope.$emit("updateBreadcrumbs", {link: "about", title: "About"});
+                $scope.statistics = data;
+            }, $scope.generalHttpFactoryError);
+        };
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+    function TribesCtrl($scope, $location, $timeout, tribesHttp) {
+        $scope.$emit("updateNavbar", {item: "nav_empty"});
+        $scope.tribes = [];
+
+        $scope.fetch = function () {
+            tribesHttp.get($scope, function (data) {
+                $scope.tribes = data;
+                $scope.$emit("updateBreadcrumbs", {link: "tribes", title: "Tribes"});
+            }, $scope.generalHttpFactoryError, true);
+        };
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+    function AdminLogCtrl($scope, logHttp, sessionRenewalHttp) {
+        $scope.logContent = "";
+        $scope.fetch = function () {
+            logHttp.get($scope, function (data) {
+                $scope.logContent = data;
+            }, $scope.generalHttpFactoryError);
+        };
+
+        $scope.deleteLog = function () {
+            logHttp.remove($scope, function () {
+                $scope.logContent = "";
+                $scope.SYNERGY.logger.log("Deleted", "", "INFO", "alert-info");
+            }, $scope.generalHttpFactoryError);
+        };
+
+        $scope.sso = function () {
+            sessionRenewalHttp.test($scope, function (d) {
+                window.console.log("Done");
+            }, function (d) {
+                window.console.log("Opps, not done at all");
+            });
+        };
+
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+    function AdminDatabaseCtrl($scope, databaseHttp) {
+        $scope.order = "ASC";
+        $scope.tables = [];
+        $scope.columns = [];
+        $scope.orderBy = "";
+        $scope.data = [];
+        $scope.limit = 10;
+        $scope.selectedTable = "";
+
+        $scope.fetch = function () {
+            databaseHttp.getTables($scope, function (data) {
+                $scope.tables = data;
+            }, $scope.generalHttpFactoryError);
+        };
+
+        $scope.loadTable = function () {
+            databaseHttp.getColumns($scope, $scope.selectedTable, function (data) {
+                $scope.columns = data;
+                $scope.orderBy = data[0];
+            }, $scope.generalHttpFactoryError);
+        };
+
+        $scope.show = function () {
+            databaseHttp.listTable($scope, $scope.selectedTable, $scope.limit || 10, $scope.order, $scope.orderBy || "id", function (data) {
+                $scope.data = data;
+            }, $scope.generalHttpFactoryError);
+        };
+
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+    }
+    function AssignmentVolunteerCtrl($scope, $routeParams, specificationsHttp, assignmentHttp, labelsHttp, platformsHttp, versionsHttp, reviewHttp, runHttp, SynergyUtils, SynergyModels) {
+        $scope.suggestions = [];
+        $scope.specifications = [];
+        $scope.assignment = {
+            testRun: $routeParams.id,
+            username: "",
+            tribe: -1,
+            specification: "",
+            label: "",
+            platform: "",
+            specificationId: "",
+            labelId: "",
+            platformId: ""
+        };
+        $scope.platforms = [];
+        $scope.ready = 0;
+        $scope.assignmentType = "test";
+        $scope.reviewUrl;
+        $scope.availablePages = [];
+        $scope.reviewPage = null;
+        $scope.showOnlyNotUsedPages = true;
+        var deleteModalDisplayed = false;
+
+        $scope.initData = function () {
+            if ($scope.assignmentType === "review") {
+                $scope.changeAvailablePages();
+            }
+        };
+
+        $scope.fetch = function () {
+            loadPlatforms();
+            loadLabels();
+            loadVersions();
+            loadSpecifications();
+        };
+
+        $scope.changeAvailablePages = function () {
+            if ($scope.showOnlyNotUsedPages) {
+                reviewHttp.listNotStarted($scope, $scope.assignment.testRun, function (d) {
+                    $scope.availablePages = d;
+                }, $scope.generalHttpFactoryError);
+            } else {
+                reviewHttp.list($scope, function (d) {
+                    $scope.availablePages = d;
+                }, $scope.generalHttpFactoryError);
+            }
+        };
+
+        function loadPlatforms() {
+            platformsHttp.get($scope, function (data) {
+                $scope.platforms = data;
+                $scope.ready++;
+                if (SynergyUtils.definedNotNull(data) && data.length > 0) {
+                    $scope.assignment.platform = data[0].id;
+                }
+            }, $scope.generalHttpFactoryError, true);
+        }
+
+        function loadVersions() {
+            versionsHttp.get($scope, true, function (data) {
+                $scope.versions = data;
+                $scope.ready++;
+                if (SynergyUtils.definedNotNull(data) && data.length > 0) {
+                    $scope.selectedVersion = data[0].name;
+                    $scope.filter();
+                }
+            }, $scope.generalHttpFactoryError);
+        }
+
+        $scope.filter = function () {
+            var matching = [];
+            for (var i = 0, max = $scope.specifications.length; i < max; i++) {
+                if ($scope.selectedVersion === $scope.specifications[i].version) {
+                    matching.push($scope.specifications[i]);
+                }
+            }
+            $scope.suggestions = matching;
+        };
+
+
+        function loadLabels() {
+            labelsHttp.getAll($scope, function (data) {
+                $scope.ready++;
+                data.push({"label": "None", "id": "-1"});
+                $scope.labels = data;
+                $scope.assignment.label = data[data.length - 1].id;
+            }, $scope.generalHttpFactoryError);
+        }
+
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+
+        function loadSpecifications() {
+            runHttp.getSpecifications($scope, $routeParams.id, function (data) {
+                $scope.specifications = [];
+                for (var i = 0, max = data.specifications.length; i < max; i += 1) {
+                    $scope.specifications[i] = {
+                        value: data.specifications[i].title + " (" + ((data.projectName === null || typeof data.projectName === "undefined") ? $scope.SYNERGY.product : data.projectName) + " " + data.specifications[i].version + ")",
+                        info: "<a href='#/specification/" + data.specifications[i].id + "'>view</a>",
+                        id: data.specifications[i].id,
+                        type: "specification",
+                        version: data.specifications[i].version
+                    };
+                }
+                $scope.filter();
+                $scope.ready++;
+            }, $scope.generalHttpFactoryError);
+        }
+
+
+        $scope.cancel = function () {
+            window.history.back();
+        };
+
+
+        function checkAssignmentExists(assignment) {
+            assignmentHttp.checkExists($scope, assignment).then(function (data) {
+                $("#deleteModal").modal("toggle");
+                deleteModalDisplayed = true;
+            }, function (response) {
+                switch (response.status) {
+                    case 404:
+                        submitAssignmentData(assignment);
+                        break;
+                    default:
+                        $scope.SYNERGY.logger.log("Action failed", "Unable to check for duplicates", "INFO", "alert-error");
+                        break;
+                }
+            });
+        }
+
+        function submitAssignmentData(assignment) {
+            $scope.showWaitDialog();
+            assignmentHttp.createVolunteer($scope, assignment, function (data) {
+                $scope.SYNERGY.modal.update("Assignment created", "");
+                $scope.SYNERGY.modal.show();
+                window.history.back();
+            }, function (data) {
+                $scope.SYNERGY.modal.show();
+                $scope.SYNERGY.logger.log("Action failed", data, "INFO", "alert-error");
+                $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+            });
+        }
+
+
+        function submitReviewAssignment() {
+            if ($scope.reviewPage === null) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+            var reviewAssignment = new SynergyModels.ReviewAssignment($scope.SYNERGY.session.username, $scope.reviewPage.url);
+            reviewAssignment.owner = $scope.reviewPage.owner;
+            reviewAssignment.title = $scope.reviewPage.title;
+            reviewAssignment.testRunId = parseInt($scope.assignment.testRun, 10);
+
+            reviewHttp.createVolunteer($scope, reviewAssignment, function (data) {
+                $scope.SYNERGY.modal.update("Assignment created", "");
+                $scope.SYNERGY.modal.show();
+                window.history.back();
+            }, function (data) {
+                $scope.SYNERGY.modal.show();
+                $scope.SYNERGY.logger.log("Action failed", data, "INFO", "alert-error");
+                $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+            });
+
+        }
+
+        /**
+         * Creates a new test assignment
+         * @returns {unresolved}
+         */
+        $scope.create = function (skipDuplicateCheck) {
+
+            if ($scope.assignmentType === "review") {
+                submitReviewAssignment();
+                return;
+            }
+
+            var assignment = new SynergyModels.TestAssignment(parseInt($scope.assignment.platform, 10), $scope.SYNERGY.session.username, parseInt($scope.assignment.label, 10));
+            assignment.specificationId = parseInt($scope.assignment.specificationId, 10);
+            assignment.testRunId = parseInt($scope.assignment.testRun, 10);
+
+            if (!$scope.assignment.specificationId || assignment.platformId < 0) {
+                $scope.SYNERGY.modal.update("Missing required fields", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+
+            if (($scope.assignedToUser && assignment.username.length < 1) || ($scope.assignedToTribe && $scope.assignment.tribe.id < 1)) {
+                $scope.SYNERGY.modal.update("Missing assignee", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+
+            if (assignment.labelId < 0 && parseInt($scope.assignment.label, 10) > 0) {
+                $scope.SYNERGY.modal.update("Label does not exist", "");
+                $scope.SYNERGY.modal.show();
+                return;
+            }
+
+            if (!skipDuplicateCheck) {
+                checkAssignmentExists(assignment);
+            } else {
+                if (deleteModalDisplayed) {
+                    $("#deleteModal").modal("toggle");
+                }
+                submitAssignmentData(assignment);
+            }
+        };
+    }
+    /**
+     * 
+     * @param {type} $scope
+     * @param {type} $routeParams
+     * @param {RevisionsFct} revisionsHttp
+     * @returns {undefined}
+     */
+    function RevisionCtrl($scope, $routeParams, revisionsHttp) {
+
+        $scope.revisions = [];
+        $scope.specificationId = $routeParams.id;
+
+        $scope.fetch = function () {
+            revisionsHttp.listRevisions($scope, $scope.specificationId, function (data) {
+                $scope.revisions = data;
+            }, $scope.generalHttpFactoryError);
+        };
+
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+        /**
+         * Loads 2 revisions from server and calls diffRevisions() to print them
+         */
+        $scope.compareRevisions = function () {
+            revisionsHttp.getRevisions($scope, $scope.choiceA, $scope.choiceB, $scope.specificationId, function (data) {
+                diffRevisions(data[0], data[1]);
+            }, $scope.generalHttpFactoryError);
+        };
+
+        function diffRevisions(rev1, rev2) {
+            var base = difflib.stringAsLines(rev1.content);
+            var newtxt = difflib.stringAsLines(rev2.content);
+            var sm = new difflib.SequenceMatcher(base, newtxt);
+            var opcodes = sm.get_opcodes();
+            var diffoutputdiv = $("#diffoutput");
+            diffoutputdiv.html("");
+
+            diffoutputdiv.append(diffview.buildView({baseTextLines: base,
+                newTextLines: newtxt,
+                opcodes: opcodes,
+                baseTextName: $scope.getLocalTime(rev1.date, true) + "(" + rev1.author + ")",
+                newTextName: $scope.getLocalTime(rev2.date, true) + "(" + rev2.author + ")",
+                contextSize: null,
+                viewType: 1}));
+        }
+    }
+    function CalendarCtrl($scope, calendarHttp) {
+        $scope.$emit("updateBreadcrumbs", {link: "calendar", title: "Calendar"});
+
+        function loadCalendar() {
+            calendarHttp.getEvents($scope, function (data) {
+                var items = [];
+
+                for (var i = 0, max = data.length; i < max; i += 1) {
+                    items.push({url: "#/run/" + data[i].id, title: data[i].title, start: new Date(data[i].start.substr(0, 4), parseInt(data[i].start.substr(4, 2), 10) - 1, data[i].start.substr(6, 2)), end: new Date(data[i].end.substr(0, 4), parseInt(data[i].end.substr(4, 2), 10) - 1, data[i].end.substr(6, 2))});
+                }
+
+                $("#calendar").fullCalendar({
+                    header: {
+                        left: "prev,next today",
+                        center: "title",
+                        right: "month,agendaWeek,agendaDay"
+                    },
+                    editable: true,
+                    events: items,
+                    eventMouseover: function (event, jsEvent, view) {
+                        if (view.name !== "agendaDay") {
+                            $(jsEvent.target).attr("title", event.title);
+                        }
+                    }
+                });
+            }, function () {
+                window.console.log("Failed to load calendar events");
+            });
+        }
+        loadCalendar();
+
+    }
+    function AssignmentTribeCtrl($scope, $routeParams, tribesHttp, labelsHttp, platformsHttp, assignmentsHttp, assignmentHttp, SynergyModels) {
+
+        $scope.tribes = [];
+        $scope.platforms = [];
+        $scope.labels = [];
+        $scope.ready = 0;
+        $scope.value = {};
+        $scope.assignments = [];
+        $scope.runId = $routeParams.id;
+
+        $scope.fetch = function () {
+            loadTribes();
+            loadLabels();
+            loadPlatforms();
+        };
+
+        function loadPlatforms() {
+            platformsHttp.get($scope, function (data) {
+                $scope.platforms = data;
+                $scope.value.platform = data[0];
+                $scope.ready++;
+            }, $scope.generalHttpFactoryError, true);
+        }
+
+        function loadLabels() {
+            labelsHttp.getAll($scope, function (data) {
+                $scope.ready++;
+                data.push({"label": "None", "id": "-1"});
+                $scope.value.label = data[data.length - 1];
+                $scope.labels = data;
+            }, $scope.generalHttpFactoryError);
+        }
+        /**
+         * Adds assignment to the array of assignments to be sent to server
+         */
+        $scope.addAssignment = function () {
+            if (!$scope.value.specification) {
+                $scope.SYNERGY.logger.showMsg("Oops", "No specification selected (maybe tribe does not have any specification?)", "alert-error");
+                return;
+            }
+            var assignment = new SynergyModels.TestAssignment(parseInt($scope.value.platform.id, 10), $scope.value.user.username, parseInt($scope.value.label.id, 10));
+            assignment.specificationId = parseInt($scope.value.specification.id, 10);
+            assignment.testRunId = parseInt($scope.runId, 10);
+            assignment.tribeId = parseInt($scope.selectedTribe.id, 10);
+            assignment.display = {
+                tribe: $scope.selectedTribe.name,
+                user: $scope.value.user.firstName + " " + $scope.value.user.lastName,
+                label: $scope.value.label.label,
+                platform: $scope.value.platform.name,
+                specification: $scope.value.specification.title + " (" + $scope.SYNERGY.product + " " + $scope.value.specification.version + ")",
+                duplicates: false
+            };
+            $scope.assignments.push(assignment);
+            assignmentHttp.checkExists($scope, assignment).then(function (data) {
+                assignment.display.duplicates = true;
+            }, function (response) {
+                switch (response.status) {
+                    case 404:
+                        assignment.display.duplicates = false;
+                        break;
+                    default:
+                        $scope.SYNERGY.logger.log("Action failed", "Unable to check for duplicates", "INFO", "alert-error");
+                        break;
+                }
+            });
+        };
+        /**
+         * Respons to change of tribe, sets preselected specification and user
+         */
+        $scope.filterTribe = function () {
+            $scope.value.user = $scope.selectedTribe.members[0];
+            if ($scope.selectedTribe.ext.specifications) {
+                $scope.value.specification = $scope.selectedTribe.ext.specifications[0];
+            }
+        };
+
+        $scope.removeAssignment = function (index) {
+            $scope.assignments.splice(index, 1);
+        };
+
+        function setProject(data) {
+            for (var t = 0, max = data.length; t < max; t++) {
+                if (data[t].hasOwnProperty("ext") && data[t].ext.hasOwnProperty("specifications")) {
+                    for (var s = 0, maxs = data[t].ext.specifications.length; s < maxs; s++) {
+                        if (data[t].ext.specifications[s].hasOwnProperty("projects") && data[t].ext.specifications[s].projects.length > 0) {
+                            data[t].ext.specifications[s]._project = data[t].ext.specifications[s].projects[0].name;
+                        } else {
+                            data[t].ext.specifications[s]._project = $scope.SYNERGY.product;
+                        }
+                    }
+                }
+            }
+        }
+
+        function loadTribes() {
+            tribesHttp.getTribesForRun($scope, $scope.SYNERGY.session.username, $scope.runId, function (data) {
+                setProject(data);
+                $scope.tribes = data;
+                $scope.selectedTribe = data[0];
+                $scope.value.user = data[0].members[0];
+                if (data[0].ext.specifications) {
+                    $scope.value.specification = data[0].ext.specifications[0];
+                }
+                $scope.ready++;
+            }, $scope.generalHttpFactoryError);
+        }
+
+        $scope.cancel = function () {
+            window.history.back();
+        };
+        $scope.create = function () {
+            assignmentsHttp.createForTribes($scope, $scope.assignments, function (data) {
+                $scope.SYNERGY.modal.update("Assignments created", "");
+                $scope.SYNERGY.modal.show();
+                window.history.back();
+            }, $scope.generalHttpFactoryError);
+        };
+
+        var self = $scope;
+        $scope.init(function () {
+            self.fetch();
+        });
+
+    }
+    function StatisticsCtrl($scope, statisticsHttp, $routeParams, SynergyUtils, SynergyModels, SynergyHandlers, SynergyIssue) {
+        $scope.id = $routeParams.id;
+        $scope.timeView = "all";
+        $scope.inArchive = window.location.href.indexOf("archive") > 0 ? true : false;
+        var archivedData = {};
+        /**
+         * Order object for Tribes table
+         */
+        $scope.orderProp = {
+            prop: "time",
+            descending: true
+        };
+
+        try {
+            $("#start").datetimepicker({dateFormat: "yy-mm-dd", timeFormat: "HH:mm:ss"});
+            $("#end").datetimepicker({dateFormat: "yy-mm-dd", timeFormat: "HH:mm:ss"});
+        } catch (e) {
+
+        }
+        $scope.P1Issues = [];
+        $scope.P2Issues = [];
+        $scope.P3Issues = [];
+        $scope.unresolvedIssues = [];
+        $scope.unknownIssues = [];
+        $scope.allIssues = [];
+        $scope.reviewTotal = {};
+
+        /**
+         * Order object for testers table
+         */
+        $scope.orderPropU = {
+            prop: "time",
+            descending: true
+        };
+        $scope.orderPropR = {
+            prop: "time",
+            descending: true
+        };
+
+        var cache = {};
+        $scope.tribes = [];
+        var tribesSpecs = [];
+        $scope.data = {};
+        $scope.computed = {};
+        var totalCounts = {};
+        var fallback = false;
+
+        $scope.fetch = function () {
+            if ($scope.inArchive) {
+                statisticsHttp.getArchive($scope, $scope.id, fallback, function (data) {
+                    $scope.data = data;
+                    cache = data;
+                    tribesSpecs = data.tribes;
+                    evaluateComputedData(data);
+                    if (data.reviews) {
+                        evaluateReviews(data.reviews);
+                    }
+                }, function (data, status) {
+                    if (status === 404 && !fallback) {
+                        $scope.SYNERGY.logger.log("Opps", "Statistics not found, trying alternative way", "INFO", "alert-warning");
+                        fallback = true;
+                        self.fetch();
+                        return;
+                    }
+                    $scope.SYNERGY.logger.log("Action failed", data, "INFO", "alert-error");
+                    $scope.SYNERGY.logger.log("Action failed", data.toString(), "DEBUG", "alert-error");
+                });
+            } else {
+                statisticsHttp.get($scope, $scope.id, function (data) {
+                    cache = data;
+                    $scope.data = data;
+                    tribesSpecs = data.tribes;
+                    evaluateComputedData(data);
+                    if (data.reviews) {
+                        evaluateReviews(data.reviews);
+                    }
+                }, $scope.generalHttpFactoryError);
+            }
+        };
+
+        $scope.getAllTimeData = function () {
+            if ($scope.timeView === "all") {
+                $scope.data = cache;
+                tribesSpecs = cache.tribes;
+                evaluateComputedData(cache);
+                if ($scope.data.reviews) {
+                    evaluateReviews($scope.data.reviews);
+                }
+            } else {
+                $("#start").val("");
+                $("#end").val("");
+            }
+        };
+
+        function evaluateReviews(reviews) {
+            var total = {
+                total: reviews.length,
+                completed: 0,
+                time: 0,
+                comments: 0,
+                reviewers: 0
+            };
+            var collector = new SynergyModels.UserReviewStats();
+            for (var i = 0, max = reviews.length; i < max; i++) {
+                if (reviews[i].isFinished) {
+                    total.completed++;
+                }
+                total.time += reviews[i].timeTaken;
+                total.comments += reviews[i].numberOfComments;
+                collector.addReview(reviews[i]);
+            }
+            total.time = Math.floor(total.time / 60) + " hours and " + (total.time % 60) + " minutes";
+            $scope.reviews = collector.finish();
+            total.reviewers = $scope.reviews.length;
+            total.finishRate = (Math.round(1000 * total.completed / total.total) / 10) || 0;
+            $scope.reviewTotal = total;
+        }
+
+        /**
+         * Loads statistics for time period
+         */
+        $scope.filterStatistics = function () {
+            var start = $("#start").val();
+            var stop = $("#end").val();
+
+            if (start.length < 1 || stop.length < 1) {
+                $scope.SYNERGY.logger.log("Missing data", "Please specify both 'from' and 'to' time periods", "INFO", "alert-error");
+                return;
+            }
+
+            if ($scope.inArchive) {
+                filterArchivedData(start, stop);
+            } else {
+                statisticsHttp.getPeriod($scope, $scope.id, {"from": $scope.getUTCTime(start), "to": $scope.getUTCTime(stop)}, function (data) {
+                    $scope.data = data;
+                    tribesSpecs = data.tribes;
+                    evaluateComputedData(data);
+                    if ($scope.data.reviews) {
+                        evaluateReviews($scope.data.reviews);
+                    }
+                }, $scope.generalHttpFactoryError);
+            }
+        };
+
+
+        function filterArchivedData(start, stop) {
+            start = SynergyUtils.localToUTCTimestamp(start);
+            stop = SynergyUtils.localToUTCTimestamp(stop);
+            $scope.data = new SynergyHandlers.ArchiveDataFilter(SynergyUtils.shallowClone(cache)).getData(start, stop);
+            tribesSpecs = $scope.data.tribes;
+            evaluateComputedData($scope.data);
+            if ($scope.data.reviews) {
+                evaluateReviews($scope.data.reviews);
+            }
+        }
+
+        $scope.sortTribes = function (prop) {
+            if ($scope.orderProp.prop === prop) {
+                $scope.orderProp.descending = !$scope.orderProp.descending;
+            } else {
+                $scope.orderProp = {
+                    prop: prop,
+                    descending: true
+                };
+            }
+        };
+
+        $scope.sortUsers = function (prop) {
+            if ($scope.orderPropU.prop === prop) {
+                $scope.orderPropU.descending = !$scope.orderPropU.descending;
+            } else {
+                $scope.orderPropU = {
+                    prop: prop,
+                    descending: true
+                };
+            }
+        };
+        $scope.sortReviewers = function (prop) {
+            if ($scope.orderPropR.prop === prop) {
+                $scope.orderPropR.descending = !$scope.orderPropR.descending;
+            } else {
+                $scope.orderPropR = {
+                    prop: prop,
+                    descending: true
+                };
+            }
+        };
+
+        /**
+         * Goes through loaded data and count overall statistics, draw charts etc.
+         */
+        function evaluateComputedData(data) {
+            var computed = {};
+            computed.testers = getTesters(data);
+            computed.testers = computed.testers.filter(function (t) {
+                return t.completedCases > 0;
+            });
+            computed.testersCount = computed.testers.length;
+            computed.time = Math.floor(totalCounts.timeToComplete / 60) + " hours and " + (totalCounts.timeToComplete % 60) + " minutes";
+            computed.completedRelative = (Math.round(1000 * data.testRun.completed / data.testRun.total) / 10) || 0;
+            computed.passRate = (Math.round(1000 * totalCounts.passed / data.testRun.completed) / 10) || 0;
+            $scope.tribes = getTribesData(data);
+            $scope.issuesTotal = data.issues.length;
+            $scope.issuesUrl = $scope.SYNERGY.issues.viewLink(data.testRun.projectName, data.issues);
+            $scope.issueColor = getIssueColor(data.issues);
+            var issueCollector = new SynergyIssue.RunIssuesCollector();
+            issueCollector.addIssues(data.issues);
+            $scope.unresolvedIssues = issueCollector.issuesStats.unresolvedIssues;
+            $scope.P1Issues = issueCollector.issuesStats.P1Issues;
... 42685 lines suppressed ...


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@netbeans.apache.org
For additional commands, e-mail: commits-help@netbeans.apache.org

For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists