You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by dp...@apache.org on 2018/10/30 17:03:37 UTC

[ignite-teamcity-bot] branch ignite-9939 created (now 6f718d2)

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

dpavlov pushed a change to branch ignite-9939
in repository https://gitbox.apache.org/repos/asf/ignite-teamcity-bot.git.


      at 6f718d2  IGNITE-9939  Add visa's caching and monitoring

This branch includes the following new commits:

     new 6f718d2  IGNITE-9939  Add visa's caching and monitoring

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[ignite-teamcity-bot] 01/01: IGNITE-9939 Add visa's caching and monitoring

Posted by dp...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dpavlov pushed a commit to branch ignite-9939
in repository https://gitbox.apache.org/repos/asf/ignite-teamcity-bot.git

commit 6f718d2460859cd808817c8fbad736acaddf6369
Author: Dmitriy Pavlov <dp...@apache.org>
AuthorDate: Tue Oct 30 20:03:26 2018 +0300

    IGNITE-9939  Add visa's caching and monitoring
---
 .../main/java/org/apache/ignite/ci/ITcHelper.java  |   9 +-
 .../main/java/org/apache/ignite/ci/TcHelper.java   | 145 +++++++++++++-------
 .../org/apache/ignite/ci/di/IgniteTcBotModule.java |   3 +-
 .../apache/ignite/ci/jira/IJiraIntegration.java    |   3 +-
 .../apache/ignite/ci/observer/BuildObserver.java   |  17 ++-
 .../org/apache/ignite/ci/observer/BuildsInfo.java  |  75 +++++++++--
 .../apache/ignite/ci/observer/ObserverTask.java    |  23 +++-
 .../tcbot/visa/TcBotTriggerAndSignOffService.java  |  65 ++++++++-
 .../visa/VisaStatus.java}                          |  39 +++---
 .../apache/ignite/ci/tcmodel/hist/BuildRef.java    |   4 +
 .../model/JiraCommentResponse.java}                |  25 ++--
 .../java/org/apache/ignite/ci/web/model/Visa.java  |  78 +++++++++++
 .../apache/ignite/ci/web/model/VisaRequest.java    |  70 ++++++++++
 .../ci/web/model/hist/VisasHistoryStorage.java     |  87 ++++++++++++
 .../ignite/ci/web/rest/visa/TcBotVisaService.java  |  16 ++-
 ignite-tc-helper-web/src/main/webapp/visas.html    | 150 +++++++++++++++++++++
 16 files changed, 695 insertions(+), 114 deletions(-)

diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/ITcHelper.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/ITcHelper.java
index 25a5bd0..4519bc2 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/ITcHelper.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/ITcHelper.java
@@ -21,9 +21,11 @@ import java.util.Collection;
 import java.util.List;
 import org.apache.ignite.ci.issue.IssueDetector;
 import org.apache.ignite.ci.issue.IssuesStorage;
+import org.apache.ignite.ci.web.model.Visa;
 import org.apache.ignite.ci.teamcity.restcached.ITcServerProvider;
 import org.apache.ignite.ci.user.ICredentialsProv;
 import org.apache.ignite.ci.user.UserAndSessionsStorage;
+import org.apache.ignite.ci.web.model.hist.VisasHistoryStorage;
 
 /**
  * Teamcity Bot main interface. This inteface became too huge.
@@ -61,13 +63,16 @@ public interface ITcHelper extends ITcServerProvider {
     /** */
     boolean isServerAuthorized();
 
+   /** */
+    VisasHistoryStorage getVisasHistoryStorage();
+
     /**
      * @param srvId Server id.
      * @param prov Credentials.
      * @param buildTypeId Suite name.
      * @param branchForTc Branch for TeamCity.
      * @param ticket JIRA ticket full name.
-     * @return {@code True} if JIRA was notified.
+     * @return {@code Visa} which contains info about JIRA notification.
      */
-    String notifyJira(String srvId, ICredentialsProv prov, String buildTypeId, String branchForTc, String ticket);
+    Visa notifyJira(String srvId, ICredentialsProv prov, String buildTypeId, String branchForTc, String ticket);
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/TcHelper.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/TcHelper.java
index b40ba69..11f3357 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/TcHelper.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/TcHelper.java
@@ -17,12 +17,15 @@
 
 package org.apache.ignite.ci;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.ignite.ci.tcbot.chain.PrChainsProcessor;
 import org.apache.ignite.ci.conf.BranchesTracked;
 import org.apache.ignite.ci.issue.IssueDetector;
 import org.apache.ignite.ci.issue.IssuesStorage;
 import org.apache.ignite.ci.jira.IJiraIntegration;
 import org.apache.ignite.ci.tcmodel.hist.BuildRef;
+import org.apache.ignite.ci.web.model.JiraCommentResponse;
+import org.apache.ignite.ci.web.model.Visa;
 import org.apache.ignite.ci.tcmodel.result.problems.ProblemOccurrence;
 import org.apache.ignite.ci.teamcity.restcached.ITcServerProvider;
 import org.apache.ignite.ci.user.ICredentialsProv;
@@ -32,6 +35,7 @@ import org.apache.ignite.ci.web.model.current.SuiteCurrentStatus;
 import org.apache.ignite.ci.web.model.current.TestFailure;
 import org.apache.ignite.ci.web.model.current.TestFailuresSummary;
 import org.apache.ignite.ci.web.model.hist.FailureSummary;
+import org.apache.ignite.ci.web.model.hist.VisasHistoryStorage;
 import org.apache.ignite.ci.web.rest.parms.FullQueryParams;
 import org.jetbrains.annotations.Nullable;
 import org.slf4j.Logger;
@@ -68,7 +72,14 @@ public class TcHelper implements ITcHelper, IJiraIntegration {
 
     @Inject private PrChainsProcessor prChainsProcessor;
 
+    /** */
+    @Inject private VisasHistoryStorage visasHistoryStorage;
+
+    /** */
+    private final ObjectMapper objectMapper;
+
     public TcHelper() {
+        objectMapper = new ObjectMapper();
     }
 
     /** {@inheritDoc} */
@@ -87,6 +98,11 @@ public class TcHelper implements ITcHelper, IJiraIntegration {
     }
 
     /** {@inheritDoc} */
+    @Override public VisasHistoryStorage getVisasHistoryStorage() {
+        return visasHistoryStorage;
+    }
+
+    /** {@inheritDoc} */
     @Override public IssuesStorage issues() {
         return issuesStorage;
     }
@@ -132,7 +148,7 @@ public class TcHelper implements ITcHelper, IJiraIntegration {
     }
 
     /** {@inheritDoc} */
-    @Override public String notifyJira(
+    @Override public Visa notifyJira(
         String srvId,
         ICredentialsProv prov,
         String buildTypeId,
@@ -144,15 +160,20 @@ public class TcHelper implements ITcHelper, IJiraIntegration {
         List<BuildRef> builds = teamcity.getFinishedBuildsIncludeSnDepFailed(buildTypeId, branchForTc);
 
         if (builds.isEmpty())
-            return "JIRA wasn't commented - no finished builds to analyze.";
+            return new Visa("JIRA wasn't commented - no finished builds to analyze.");
 
         BuildRef build = builds.get(builds.size() - 1);
-        String comment;
+
+        List<SuiteCurrentStatus> suitesStatuses;
+
+        JiraCommentResponse response;
 
         try {
-            comment = generateJiraComment(buildTypeId, build.branchName, srvId, prov, build.webUrl);
+            suitesStatuses =  getSuitesStatuses(buildTypeId, build.branchName, srvId, prov);
+
+            String comment = generateJiraComment(suitesStatuses, build.webUrl);
 
-            teamcity.sendJiraComment(ticket, comment);
+            response = objectMapper.readValue(teamcity.sendJiraComment(ticket, comment), JiraCommentResponse.class);
         }
         catch (Exception e) {
             String errMsg = "Exception happened during commenting JIRA ticket " +
@@ -160,10 +181,10 @@ public class TcHelper implements ITcHelper, IJiraIntegration {
 
             logger.error(errMsg);
 
-            return "JIRA wasn't commented - " + errMsg;
+            return new Visa("JIRA wasn't commented - " + errMsg);
         }
 
-        return JIRA_COMMENTED;
+        return new Visa(JIRA_COMMENTED, response, suitesStatuses);
     }
 
     /**
@@ -171,17 +192,14 @@ public class TcHelper implements ITcHelper, IJiraIntegration {
      * @param branchForTc Branch for TeamCity.
      * @param srvId Server id.
      * @param prov Credentials.
-     * @param webUrl Build URL.
-     * @return Comment, which should be sent to the JIRA ticket.
+     * @return List of suites with possible blockers.
      */
-    private String generateJiraComment(
-        String buildTypeId,
+    public List<SuiteCurrentStatus> getSuitesStatuses(String buildTypeId,
         String branchForTc,
         String srvId,
-        ICredentialsProv prov,
-        String webUrl
-    ) {
-        StringBuilder res = new StringBuilder();
+        ICredentialsProv prov) {
+        List<SuiteCurrentStatus> res = new ArrayList<>();
+
         TestFailuresSummary summary = prChainsProcessor.getTestFailuresSummary(
             prov, srvId, buildTypeId, branchForTc,
             FullQueryParams.LATEST, null, null, false);
@@ -193,54 +211,61 @@ public class TcHelper implements ITcHelper, IJiraIntegration {
 
                 Map<String, List<SuiteCurrentStatus>> fails = findFailures(server);
 
-                for (List<SuiteCurrentStatus> suites : fails.values()) {
-                    for (SuiteCurrentStatus suite : suites) {
-                        res.append("{color:#d04437}").append(suite.name).append("{color}");
-                        res.append(" [[tests ").append(suite.failedTests);
+                fails.forEach((k, v) -> res.addAll(v));
+            }
+        }
 
-                        if (suite.result != null && !suite.result.isEmpty())
-                            res.append(' ').append(suite.result);
+        return res;
+    }
 
-                        res.append('|').append(suite.webToBuild).append("]]\\n");
+    /** */
+    private String generateJiraComment(List<SuiteCurrentStatus> suites, String webUrl) {
+        StringBuilder res = new StringBuilder();
 
-                        for (TestFailure failure : suite.testFailures) {
-                            res.append("* ");
+        for (SuiteCurrentStatus suite : suites) {
+            res.append("{color:#d04437}").append(suite.name).append("{color}");
+            res.append(" [[tests ").append(suite.failedTests);
 
-                            if (failure.suiteName != null && failure.testName != null)
-                                res.append(failure.suiteName).append(": ").append(failure.testName);
-                            else
-                                res.append(failure.name);
+            if (suite.result != null && !suite.result.isEmpty())
+                res.append(' ').append(suite.result);
 
-                            FailureSummary recent = failure.histBaseBranch.recent;
+            res.append('|').append(suite.webToBuild).append("]]\\n");
 
-                            if (recent != null) {
-                                if (recent.failureRate != null) {
-                                    res.append(" - ").append(recent.failureRate).append("% fails in last ")
-                                        .append(MAX_LATEST_RUNS).append(" master runs.");
-                                }
-                                else if (recent.failures != null && recent.runs != null) {
-                                    res.append(" - ").append(recent.failures).append(" fails / ")
-                                        .append(recent.runs).append(" runs.");
-                                }
-                            }
+            for (TestFailure failure : suite.testFailures) {
+                res.append("* ");
 
-                            res.append("\\n");
-                        }
+                if (failure.suiteName != null && failure.testName != null)
+                    res.append(failure.suiteName).append(": ").append(failure.testName);
+                else
+                    res.append(failure.name);
 
-                        res.append("\\n");
+                FailureSummary recent = failure.histBaseBranch.recent;
+
+                if (recent != null) {
+                    if (recent.failureRate != null) {
+                        res.append(" - ").append(recent.failureRate).append("% fails in last ")
+                            .append(MAX_LATEST_RUNS).append(" master runs.");
+                    }
+                    else if (recent.failures != null && recent.runs != null) {
+                        res.append(" - ").append(recent.failures).append(" fails / ")
+                            .append(recent.runs).append(" runs.");
                     }
                 }
 
-                if (res.length() > 0) {
-                    res.insert(0, "{panel:title=Possible Blockers|" +
-                        "borderStyle=dashed|borderColor=#ccc|titleBGColor=#F7D6C1}\\n")
-                        .append("{panel}");
-                }
-                else {
-                    res.append("{panel:title=No blockers found!|" +
-                        "borderStyle=dashed|borderColor=#ccc|titleBGColor=#D6F7C1}{panel}");
-                }
+                res.append("\\n");
             }
+
+            res.append("\\n");
+        }
+
+        if (res.length() > 0) {
+            res.insert(0, "{panel:title=Possible Blockers|" +
+                "borderStyle=dashed|borderColor=#ccc|titleBGColor=#F7D6C1}\\n")
+                .append("{panel}");
+        }
+        else {
+            res.append("{panel:title=No blockers found!|" +
+                "borderStyle=dashed|borderColor=#ccc|titleBGColor=#D6F7C1}{panel}");
         }
 
         res.append("\\n").append("[TeamCity Run All Results|").append(webUrl).append(']');
@@ -249,6 +274,24 @@ public class TcHelper implements ITcHelper, IJiraIntegration {
     }
 
     /**
+     * @param buildTypeId Suite name.
+     * @param branchForTc Branch for TeamCity.
+     * @param srvId Server id.
+     * @param prov Credentials.
+     * @param webUrl Build URL.
+     * @return Comment, which should be sent to the JIRA ticket.
+     */
+    private String generateJiraComment(
+        String buildTypeId,
+        String branchForTc,
+        String srvId,
+        ICredentialsProv prov,
+        String webUrl
+    ) {
+        return generateJiraComment(getSuitesStatuses(buildTypeId, branchForTc,srvId, prov), webUrl);
+    }
+
+    /**
      * @param srv Server.
      * @return Failures for given server.
      */
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/di/IgniteTcBotModule.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/di/IgniteTcBotModule.java
index b2aa2d8..cbfe973 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/di/IgniteTcBotModule.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/di/IgniteTcBotModule.java
@@ -37,6 +37,7 @@ import org.apache.ignite.ci.issue.IssueDetector;
 import org.apache.ignite.ci.jira.IJiraIntegration;
 import org.apache.ignite.ci.observer.BuildObserver;
 import org.apache.ignite.ci.observer.ObserverTask;
+import org.apache.ignite.ci.web.model.Visa;
 import org.apache.ignite.ci.teamcity.ignited.TeamcityIgnitedModule;
 import org.apache.ignite.ci.user.ICredentialsProv;
 import org.apache.ignite.ci.util.ExceptionUtil;
@@ -90,7 +91,7 @@ public class IgniteTcBotModule extends AbstractModule {
     private static class Jira implements IJiraIntegration {
         @Inject ITcHelper helper;
 
-        @Override public String notifyJira(String srvId, ICredentialsProv prov, String buildTypeId, String branchForTc,
+        @Override public Visa notifyJira(String srvId, ICredentialsProv prov, String buildTypeId, String branchForTc,
             String ticket) {
             return helper.notifyJira(srvId, prov, buildTypeId, branchForTc, ticket);
         }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java
index a20ead4..aa1084c 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.ci.jira;
 
+import org.apache.ignite.ci.web.model.Visa;
 import org.apache.ignite.ci.user.ICredentialsProv;
 
 /**
@@ -34,6 +35,6 @@ public interface IJiraIntegration {
      * @param ticket JIRA ticket full name. E.g. IGNITE-5555
      * @return {@code True} if JIRA was notified.
      */
-    public String notifyJira(String srvId, ICredentialsProv prov, String buildTypeId, String branchForTc,
+    public Visa notifyJira(String srvId, ICredentialsProv prov, String buildTypeId, String branchForTc,
         String ticket);
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildObserver.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildObserver.java
index 1e08eb8..27814da 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildObserver.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildObserver.java
@@ -21,8 +21,10 @@ import java.util.Collection;
 import java.util.Objects;
 import java.util.Timer;
 import javax.inject.Inject;
+import org.apache.ignite.ci.ITcHelper;
 import org.apache.ignite.ci.tcmodel.result.Build;
 import org.apache.ignite.ci.user.ICredentialsProv;
+import org.apache.ignite.ci.web.model.VisaRequest;
 
 /**
  *
@@ -37,6 +39,9 @@ public class BuildObserver {
     /** Task, which should be done periodically. */
     private ObserverTask observerTask;
 
+    /** Helper. */
+    @Inject ITcHelper helper;
+
     /**
      */
     @Inject
@@ -58,10 +63,16 @@ public class BuildObserver {
     /**
      * @param srvId Server id.
      * @param prov Credentials.
+     * @param ticket Ticket.
+     * @param branchForTc Branch for TC.
      * @param ticket JIRA ticket name.
      */
-    public void observe(String srvId, ICredentialsProv prov, String ticket, Build... builds) {
-        observerTask.addBuild(new BuildsInfo(srvId, prov, ticket, builds));
+    public void observe(String srvId, ICredentialsProv prov, String ticket, String branchForTc, Build... builds) {
+        BuildsInfo buildsInfo = new BuildsInfo(srvId, prov, ticket, branchForTc, builds);
+
+        helper.getVisasHistoryStorage().put(new VisaRequest(buildsInfo));
+
+        observerTask.addBuild(buildsInfo);
     }
 
     /**
@@ -73,7 +84,7 @@ public class BuildObserver {
         Collection<BuildsInfo> builds = observerTask.getBuilds();
 
         for (BuildsInfo bi : builds) {
-            if (Objects.equals(bi.branchName, branch)
+            if (Objects.equals(bi.branchForTc, branch)
                 && Objects.equals(bi.srvId, srvId)) {
                 sb.append(bi.ticket).append(" to be commented, waiting for builds. ");
                 sb.append(bi.finishedBuildsCount());
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildsInfo.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildsInfo.java
index ba8b48a..439117f 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildsInfo.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildsInfo.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.ci.observer;
 
+import java.util.Calendar;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
@@ -28,6 +30,18 @@ import org.apache.ignite.ci.user.ICredentialsProv;
  *
  */
 public class BuildsInfo {
+    /** */
+    public static final String FINISHED_STATE = "finished";
+
+    /** */
+    public static final String RUNNING_STATE = "running";
+
+    /** */
+    public static final String FINISHED_WITH_FAILURES_STATE = "finished with failures";
+
+    /** */
+    public final String userName;
+
     /** Server id. */
     public final String srvId;
 
@@ -35,42 +49,74 @@ public class BuildsInfo {
     public final String buildTypeId;
 
     /** Branch name. */
-    public final String branchName;
+    public final String branchForTc;
 
     /** JIRA ticket full name. */
     public final String ticket;
 
+    /** */
+    public final Date date;
+
     /** Finished builds. */
-    private final Map<Build, Boolean> finishedBuilds = new HashMap<>();
+    private final Map<Integer, Boolean> finishedBuilds = new HashMap<>();
 
     /**
      * @param srvId Server id.
      * @param prov Prov.
+     * @param branchForTc Branch for TC.
      * @param ticket Ticket.
      * @param builds Builds.
      */
-    public BuildsInfo(String srvId, ICredentialsProv prov, String ticket, Build[] builds) {
+    public BuildsInfo(String srvId, ICredentialsProv prov, String ticket, String branchForTc, Build... builds) {
+        this.userName = prov.getUser(srvId);
+        this.date = Calendar.getInstance().getTime();
         this.srvId = srvId;
         this.ticket = ticket;
-        this.buildTypeId = builds.length > 1 ? "IgniteTests24Java8_RunAll" : builds[0].buildTypeId;
-        this.branchName = builds[0].branchName;
+        this.branchForTc = branchForTc;
+        this.buildTypeId = builds.length == 1 ? builds[0].buildTypeId : "IgniteTests24Java8_RunAll";
 
         for (Build build : builds)
-            finishedBuilds.put(build, false);
+            finishedBuilds.put(build.getId(), false);
     }
 
     /**
      * @param teamcity Teamcity.
      */
-    public boolean isFinished(IAnalyticsEnabledTeamcity teamcity) {
-        for (Map.Entry<Build, Boolean> entry : finishedBuilds.entrySet()) {
+    public String getState(IAnalyticsEnabledTeamcity teamcity) {
+        for (Map.Entry<Integer, Boolean> entry : finishedBuilds.entrySet()) {
+            if (entry.getValue() == null)
+                return FINISHED_WITH_FAILURES_STATE;
+
             if (!entry.getValue()) {
-                Build build = teamcity.getBuild(entry.getKey().getId());
-                entry.setValue(build.isFinished());
+                Build build = teamcity.getBuild(entry.getKey());
+
+                if (build.isFinished()) {
+                    if (build.isUnknown()) {
+                        entry.setValue(null);
+
+                        return FINISHED_WITH_FAILURES_STATE;
+                    }
+
+                    entry.setValue(true);
+                }
             }
         }
 
-        return !finishedBuilds.containsValue(false);
+        return finishedBuilds.containsValue(false) ? RUNNING_STATE : FINISHED_STATE;
+    }
+
+    /**
+     * @param teamcity Teamcity.
+     */
+    public boolean isFinished(IAnalyticsEnabledTeamcity teamcity) {
+        return FINISHED_STATE.equals(getState(teamcity));
+    }
+
+    /**
+     * @param teamcity Teamcity.
+     */
+    public boolean isFinishedWithFailures(IAnalyticsEnabledTeamcity teamcity) {
+        return FINISHED_WITH_FAILURES_STATE.equals(getState(teamcity));
     }
 
     /**
@@ -99,13 +145,14 @@ public class BuildsInfo {
 
         return Objects.equals(srvId, info.srvId) &&
             Objects.equals(buildTypeId, info.buildTypeId) &&
-            Objects.equals(branchName, info.branchName) &&
+            Objects.equals(branchForTc, info.branchForTc) &&
             Objects.equals(ticket, info.ticket) &&
-            Objects.equals(finishedBuilds.keySet(), info.finishedBuilds.keySet());
+            Objects.equals(finishedBuilds.keySet(), info.finishedBuilds.keySet()) &&
+            Objects.equals(date, info.date);
     }
 
     /** {@inheritDoc} */
     @Override public int hashCode() {
-        return Objects.hash(srvId, buildTypeId, branchName, ticket, finishedBuilds.keySet());
+        return Objects.hash(srvId, buildTypeId, branchForTc, ticket, finishedBuilds.keySet(), date);
     }
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/ObserverTask.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/ObserverTask.java
index 37d6e0f..7083a9c 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/ObserverTask.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/ObserverTask.java
@@ -32,11 +32,10 @@ import org.apache.ignite.ci.di.MonitoredTask;
 import org.apache.ignite.ci.jira.IJiraIntegration;
 import org.apache.ignite.ci.user.ICredentialsProv;
 import org.apache.ignite.configuration.CollectionConfiguration;
+import org.apache.ignite.ci.web.model.Visa;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import static org.apache.ignite.ci.jira.IJiraIntegration.JIRA_COMMENTED;
-
 /**
  * Checks observed builds for finished status and comments JIRA ticket.
  */
@@ -64,7 +63,7 @@ public class ObserverTask extends TimerTask {
 
         cfg.setBackups(1);
 
-        return ignite.queue("buildsQueue", 0, cfg);
+        return ignite.queue("builds", 0, cfg);
     }
 
     /** */
@@ -107,6 +106,16 @@ public class ObserverTask extends TimerTask {
 
             IAnalyticsEnabledTeamcity teamcity = tcHelper.server(info.srvId, tcHelper.getServerAuthorizerCreds());
 
+            if (info.isFinishedWithFailures(teamcity)) {
+                builds.remove(info);
+
+                logger.error("JIRA will not be commented." +
+                    " [ticket: " + info.ticket + ", branch:" + info.branchForTc + "] : " +
+                    "one or more re-runned blocker's builds finished with UNKNOWN status.");
+
+                continue;
+            }
+
             if (!info.isFinished(teamcity)) {
                 notFinishedBuilds += info.buildsCount() - info.finishedBuildsCount();
 
@@ -115,10 +124,12 @@ public class ObserverTask extends TimerTask {
 
             ICredentialsProv creds = tcHelper.getServerAuthorizerCreds();
 
-            String jiraRes = jiraIntegration.notifyJira(info.srvId, creds, info.buildTypeId,
-                info.branchName, info.ticket);
+            Visa visa = jiraIntegration.notifyJira(info.srvId, creds, info.buildTypeId,
+                info.branchForTc, info.ticket);
+
+            tcHelper.getVisasHistoryStorage().updateVisaRequestResult(info.date, visa);
 
-            if (JIRA_COMMENTED.equals(jiraRes)) {
+            if (visa.isSuccess()) {
                 ticketsNotified.add(info.ticket);
 
                 builds.remove(info);
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/TcBotTriggerAndSignOffService.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/TcBotTriggerAndSignOffService.java
index ecc4267..fd75cca 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/TcBotTriggerAndSignOffService.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/TcBotTriggerAndSignOffService.java
@@ -19,6 +19,8 @@ package org.apache.ignite.ci.tcbot.visa;
 
 import com.google.common.base.Strings;
 import com.google.inject.Provider;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
@@ -28,6 +30,7 @@ import javax.annotation.Nonnull;
 import javax.inject.Inject;
 import javax.ws.rs.QueryParam;
 import org.apache.ignite.ci.ITcHelper;
+import org.apache.ignite.ci.IAnalyticsEnabledTeamcity;
 import org.apache.ignite.ci.github.GitHubUser;
 import org.apache.ignite.ci.github.PullRequest;
 import org.apache.ignite.ci.github.ignited.IGitHubConnIgnitedProvider;
@@ -36,9 +39,12 @@ import org.apache.ignite.ci.github.pure.IGitHubConnectionProvider;
 import org.apache.ignite.ci.jira.IJiraIntegration;
 import org.apache.ignite.ci.observer.BuildObserver;
 import org.apache.ignite.ci.tcmodel.hist.BuildRef;
+import org.apache.ignite.ci.observer.BuildsInfo;
 import org.apache.ignite.ci.tcmodel.result.Build;
 import org.apache.ignite.ci.teamcity.ignited.ITeamcityIgnited;
 import org.apache.ignite.ci.teamcity.ignited.ITeamcityIgnitedProvider;
+import org.apache.ignite.ci.web.model.VisaRequest;
+import org.apache.ignite.ci.web.model.Visa;
 import org.apache.ignite.ci.user.ICredentialsProv;
 import org.apache.ignite.ci.web.model.SimpleResult;
 import org.apache.ignite.internal.util.typedef.F;
@@ -70,10 +76,57 @@ public class TcBotTriggerAndSignOffService {
     @Inject ITcHelper tcHelper;
 
     /** */
+    SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+    /** */
     public void startObserver() {
         buildObserverProvider.get();
     }
 
+    /** */
+    public List<VisaStatus> getVisasStatus(String srvId, ICredentialsProv prov) {
+        List<VisaStatus> visaStatuses = new ArrayList<>();
+
+        IAnalyticsEnabledTeamcity teamcity = tcHelper.server(srvId, prov);
+
+        for (VisaRequest visaRequest : tcHelper.getVisasHistoryStorage().getVisas()) {
+            VisaStatus visaStatus = new VisaStatus();
+
+            BuildsInfo info = visaRequest.getInfo();
+
+            Visa visa = visaRequest.getResult();
+
+            visaStatus.date = formatter.format(info.date);
+            visaStatus.branchName = info.branchForTc;
+            visaStatus.userName = info.userName;
+            visaStatus.ticket = info.ticket;
+
+            if (info.isFinished(teamcity)) {
+                if (visa != null && visa.isSuccess()) {
+                    visaStatus.commentUrl = "https://issues.apache.org/jira/browse/" + visaStatus.ticket +
+                        "?focusedCommentId=" + visa.getJiraCommentResponse().getId() +
+                        "&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-" +
+                        visa.getJiraCommentResponse().getId();
+
+                    visaStatus.blockers = visa.getSuitesStatuses().stream().mapToInt(suite ->
+                        suite.testFailures.size()).sum();
+
+                    visaStatus.state = BuildsInfo.FINISHED_STATE;
+                }
+                else if (visa != null && !visa.isSuccess())
+                    visaStatus.state = BuildsInfo.FINISHED_WITH_FAILURES_STATE;
+                else if (visa == null)
+                    visaStatus.state = BuildsInfo.FINISHED_STATE + " [ waiting results ]";
+            }
+            else
+                visaStatus.state = info.getState(teamcity);
+
+            visaStatuses.add(visaStatus);
+        }
+
+        return visaStatuses;
+    }
+
     /**
      * @param pr Pull Request.
      * @return JIRA ticket full name or empty string.
@@ -94,7 +147,6 @@ public class TcBotTriggerAndSignOffService {
         return ticketId;
     }
 
-
     @NotNull public String triggerBuildsAndObserve(
         @Nullable String srvId,
         @Nullable String branchForTc,
@@ -161,7 +213,7 @@ public class TcBotTriggerAndSignOffService {
             ticketFullName = ticketFullName.toUpperCase().startsWith("IGNITE-") ? ticketFullName : "IGNITE-" + ticketFullName;
         }
 
-        buildObserverProvider.get().observe(srvId, prov, ticketFullName, builds);
+        buildObserverProvider.get().observe(srvId, prov, ticketFullName, branchForTc, builds);
 
         if (!tcHelper.isServerAuthorized())
             return "Ask server administrator to authorize the Bot to enable JIRA notifications.";
@@ -211,7 +263,14 @@ public class TcBotTriggerAndSignOffService {
         }
 
         if (!Strings.isNullOrEmpty(ticketFullName)) {
-            jiraRes = jiraIntegration.notifyJira(srvId, prov, suiteId, branchForTc, ticketFullName);
+            BuildsInfo buildsInfo = new BuildsInfo(srvId, prov, ticketFullName, branchForTc);
+
+            Visa visa = jiraIntegration.notifyJira(srvId, prov, suiteId, branchForTc, ticketFullName);
+
+            tcHelper.getVisasHistoryStorage().put(new VisaRequest(buildsInfo)
+                .setResult(visa));
+
+            jiraRes = visa.getStatus();
 
             return new SimpleResult(jiraRes);
         }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/VisaStatus.java
similarity index 53%
copy from ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java
copy to ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/VisaStatus.java
index a20ead4..1dfe31f 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/VisaStatus.java
@@ -15,25 +15,32 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.ci.jira;
+package org.apache.ignite.ci.tcbot.visa;
 
-import org.apache.ignite.ci.user.ICredentialsProv;
+import org.jetbrains.annotations.Nullable;
 
 /**
  *
  */
-public interface IJiraIntegration {
-    /** Message to show user when JIRA ticket was successfully commented by the Bot. */
-    public static String JIRA_COMMENTED = "JIRA commented.";
-
-    /**
-     * @param srvId TC Server ID to take information about token from.
-     * @param prov Credentials.
-     * @param buildTypeId Suite name.
-     * @param branchForTc Branch for TeamCity.
-     * @param ticket JIRA ticket full name. E.g. IGNITE-5555
-     * @return {@code True} if JIRA was notified.
-     */
-    public String notifyJira(String srvId, ICredentialsProv prov, String buildTypeId, String branchForTc,
-        String ticket);
+public class VisaStatus {
+    /** */
+    @Nullable public String userName;
+
+    /** Branch name. */
+    @Nullable public String branchName;
+
+    /** JIRA ticket full name. */
+    @Nullable public String ticket;
+
+    /** */
+    @Nullable public String state;
+
+    /** */
+    @Nullable public String commentUrl;
+
+    /** */
+    @Nullable public String date;
+
+    /** */
+    public int blockers;
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/hist/BuildRef.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/hist/BuildRef.java
index 0ac3293..bdb5051 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/hist/BuildRef.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/hist/BuildRef.java
@@ -187,4 +187,8 @@ public class BuildRef extends AbstractRef {
     public boolean isRunning() {
         return STATE_RUNNING.equals(state());
     }
+
+    public boolean isUnknown() {
+        return STATUS_UNKNOWN.equals(status());
+    }
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/JiraCommentResponse.java
similarity index 53%
copy from ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java
copy to ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/JiraCommentResponse.java
index a20ead4..26bde0a 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/JiraCommentResponse.java
@@ -15,25 +15,20 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.ci.jira;
+package org.apache.ignite.ci.web.model;
 
-import org.apache.ignite.ci.user.ICredentialsProv;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 
 /**
  *
  */
-public interface IJiraIntegration {
-    /** Message to show user when JIRA ticket was successfully commented by the Bot. */
-    public static String JIRA_COMMENTED = "JIRA commented.";
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class JiraCommentResponse {
+    /** */
+    private int id;
 
-    /**
-     * @param srvId TC Server ID to take information about token from.
-     * @param prov Credentials.
-     * @param buildTypeId Suite name.
-     * @param branchForTc Branch for TeamCity.
-     * @param ticket JIRA ticket full name. E.g. IGNITE-5555
-     * @return {@code True} if JIRA was notified.
-     */
-    public String notifyJira(String srvId, ICredentialsProv prov, String buildTypeId, String branchForTc,
-        String ticket);
+    /** */
+    public int getId() {
+        return id;
+    }
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/Visa.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/Visa.java
new file mode 100644
index 0000000..7177333
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/Visa.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.ci.web.model;
+
+import java.util.List;
+import org.apache.ignite.ci.jira.IJiraIntegration;
+import org.apache.ignite.ci.web.model.current.SuiteCurrentStatus;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ *
+ */
+public class Visa {
+    /** */
+    private final String status;
+
+    /** */
+    @Nullable private final JiraCommentResponse jiraCommentResponse;
+
+    /** */
+    @Nullable private final List<SuiteCurrentStatus> suitesStatuses;
+
+    /** */
+    @Nullable public String getStatus() {
+        return status;
+    }
+
+    /** */
+    public Visa(String status) {
+        this.status = status;
+        this.jiraCommentResponse = null;
+        this.suitesStatuses = null;
+    }
+
+    /** */
+    public Visa(String status, JiraCommentResponse response, List<SuiteCurrentStatus> suitesStasuses) {
+        this.status = status;
+        this.jiraCommentResponse = response;
+        this.suitesStatuses = suitesStasuses;
+    }
+
+    /** */
+    @Nullable public JiraCommentResponse getJiraCommentResponse() {
+        return jiraCommentResponse;
+    }
+
+    /** */
+    @Nullable public List<SuiteCurrentStatus> getSuitesStatuses() {
+        return suitesStatuses;
+    }
+
+    /** */
+    public boolean isSuccess() {
+        return IJiraIntegration.JIRA_COMMENTED.equals(status) 
+            && jiraCommentResponse != null 
+            && suitesStatuses != null;
+    }
+
+    /** */
+    @Override public String toString() {
+        return status;
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/VisaRequest.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/VisaRequest.java
new file mode 100644
index 0000000..c27d5ad
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/VisaRequest.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.ci.web.model;
+
+import java.util.Objects;
+import org.apache.ignite.ci.IAnalyticsEnabledTeamcity;
+import org.apache.ignite.ci.observer.BuildsInfo;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ *
+ */
+public class VisaRequest {
+    /** */
+    private BuildsInfo buildsInfo;
+
+    /** */
+    private Visa result;
+
+    /** */
+    public VisaRequest(BuildsInfo buildsInfo) {
+        this.buildsInfo = buildsInfo;
+    }
+
+    /** */
+    public BuildsInfo getInfo() {
+        return buildsInfo;
+    }
+
+    /** */
+    @Nullable public Visa getResult() {
+        return result;
+    }
+
+    /** */
+    public VisaRequest setResult(Visa result) {
+        this.result = result;
+
+        return this;
+    }
+
+    /**
+     * @param teamcity Teamcity.
+     */
+    public boolean isFinished(IAnalyticsEnabledTeamcity teamcity) {
+        return buildsInfo.isFinished(teamcity) && !Objects.isNull(result);
+    }
+
+    /**
+     * @param teamcity Teamcity.
+     */
+    public boolean isFinishedWithFailures(IAnalyticsEnabledTeamcity teamcity) {
+        return buildsInfo.isFinishedWithFailures(teamcity);
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/hist/VisasHistoryStorage.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/hist/VisasHistoryStorage.java
new file mode 100644
index 0000000..5b921b9
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/hist/VisasHistoryStorage.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.ci.web.model.hist;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import javax.cache.Cache;
+import javax.inject.Inject;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.ci.db.TcHelperDb;
+import org.apache.ignite.ci.web.model.Visa;
+import org.apache.ignite.ci.web.model.VisaRequest;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ *
+ */
+public class VisasHistoryStorage {
+    /** */
+    private static final String VISAS_CACHE_NAME = "visasCache";
+
+    /** */
+    @Inject
+    private Ignite ignite;
+
+    /** */
+    public void clear() {
+        visas().clear();
+    }
+
+    /** */
+    private Cache<Date, VisaRequest> visas() {
+        return ignite.getOrCreateCache(TcHelperDb.getCacheV2TxConfig(VISAS_CACHE_NAME));
+    }
+
+    /** */
+    public void put(VisaRequest visaRequest) {
+        visas().put(visaRequest.getInfo().date, visaRequest);
+    }
+
+    /** */
+    @Nullable public VisaRequest get(Date date) {
+        return visas().get(date);
+    }
+
+    /** */
+    public boolean updateVisaRequestResult(Date date, Visa visa) {
+        VisaRequest req = visas().get(date);
+
+        if (Objects.isNull(req))
+            return false;
+
+        req.setResult(visa);
+
+        put(req);
+
+        return true;
+    }
+
+    /** */
+    public Collection<VisaRequest> getVisas() {
+        List<VisaRequest> res = new ArrayList<>();
+
+        visas().forEach(entry -> res.add(entry.getValue()));
+
+        return Collections.unmodifiableCollection(res);
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/visa/TcBotVisaService.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/visa/TcBotVisaService.java
index 5bc1cbf..5b52dfa 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/visa/TcBotVisaService.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/visa/TcBotVisaService.java
@@ -16,6 +16,7 @@
  */
 package org.apache.ignite.ci.web.rest.visa;
 
+import java.util.Collection;
 import java.util.List;
 import javax.annotation.Nonnull;
 import javax.servlet.ServletContext;
@@ -27,11 +28,11 @@ import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import org.apache.ignite.ci.tcbot.visa.ContributionCheckStatus;
-import org.apache.ignite.ci.user.ICredentialsProv;
 import org.apache.ignite.ci.tcbot.visa.ContributionToCheck;
 import org.apache.ignite.ci.tcbot.visa.TcBotTriggerAndSignOffService;
+import org.apache.ignite.ci.tcbot.visa.VisaStatus;
+import org.apache.ignite.ci.user.ICredentialsProv;
 import org.apache.ignite.ci.web.CtxListener;
-import org.apache.ignite.ci.web.model.SimpleResult;
 import org.apache.ignite.ci.web.rest.exception.ServiceUnauthorizedException;
 import org.jetbrains.annotations.Nullable;
 
@@ -50,6 +51,17 @@ public class TcBotVisaService {
      * @param srvId Server id.
      */
     @GET
+    @Path("history")
+    public Collection<VisaStatus> history(@Nullable @QueryParam("serverId") String srvId) {
+        return CtxListener.getInjector(ctx)
+            .getInstance(TcBotTriggerAndSignOffService.class)
+            .getVisasStatus(srvId, ICredentialsProv.get(req));
+    }
+
+    /**
+     * @param srvId Server id.
+     */
+    @GET
     @Path("contributions")
     public List<ContributionToCheck> contributions(@Nullable @QueryParam("serverId") String srvId) {
         if (!ICredentialsProv.get(req).hasAccess(srvId))
diff --git a/ignite-tc-helper-web/src/main/webapp/visas.html b/ignite-tc-helper-web/src/main/webapp/visas.html
new file mode 100644
index 0000000..87c1864
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/webapp/visas.html
@@ -0,0 +1,150 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="UTF-8">
+        <title>Ignite Teamcity - Visas history</title>
+        <link rel="icon" href="img/leaf-icon-png-7066.png">
+        <script src="https://code.jquery.com/jquery-1.12.4.js"></script>
+        <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
+        <script type="text/javascript" src="https://cdn.jsdelivr.net/momentjs/latest/moment.min.js"></script>
+        <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.min.js"></script>
+        <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css" />
+        <link rel="stylesheet" href="css/style-1.5.css">
+        <link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
+        <script src="js/common-1.6.js"></script>
+        <script src="https://d3js.org/d3.v4.min.js"></script>
+        <script src="https://cdn.datatables.net/1.10.16/js/jquery.dataTables.js"></script>
+        <script src="https://cdn.datatables.net/1.10.16/js/dataTables.jqueryui.js"></script>
+        <link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
+        <link rel="stylesheet" href="https://cdn.datatables.net/1.10.16/css/jquery.dataTables.min.css">
+    </head>
+<body>
+    <br>
+    <div id="loadStatus"></div>
+    <br>
+    <table id="visasTable" class="cell-border" style="width:100%">
+        <thead>
+            <tr class="ui-widget-header ">
+                <th>.</th>
+                <th>.</th>
+                <th>.</th>
+                <th>.</th>
+                <th>.</th>
+                <th>.</th>
+            </tr>
+        </thead>
+    </table>
+    <br>
+    <div id="version"></div>
+<script>
+function showErrInLoadStatus(jqXHR, exception) {
+    if (jqXHR.status === 0) {
+        $("#loadStatus").html('Not connect.\n Verify Network.');
+    } else if (jqXHR.status === 404) {
+        $("#loadStatus").html('Requested page not found. [404]');
+    } else if (jqXHR.status === 401) {
+        $("#loadStatus").html('Unauthorized [401]');
+
+        setTimeout(function() {
+            window.location.href = "/login.html" + "?backref=" + encodeURIComponent(window.location.href);
+        }, 1000);
+    } else if (jqXHR.status === 403) {
+        $("#loadStatus").html('Forbidden [403]');
+    } else if( jqXHR.status === 418) {
+        $("#loadStatus").html('Services are starting [418], I\'m a teapot');
+    } else if (jqXHR.status === 424) {
+        $("#loadStatus").html('Dependency problem: [424]: ' + jqXHR.responseText);
+    } else if (jqXHR.status === 500) {
+        $("#loadStatus").html('Internal Server Error [500].');
+    } else if (exception === 'parsererror') {
+        $("#loadStatus").html('Requested JSON parse failed.');
+    } else if (exception === 'timeout') {
+        $("#loadStatus").html('Time out error.');
+    } else if (exception === 'abort') {
+        $("#loadStatus").html('Ajax request aborted.');
+    } else {
+        $("#loadStatus").html('Uncaught Error.\n' + jqXHR.responseText);
+    }
+}
+
+$(document).ready(function() {
+    loadData();
+
+    $.ajax({ url: "rest/branches/version",  success: showVersionInfo, error: showErrInLoadStatus });
+});
+
+function showVisasTable(result) {
+    let visasTable = $('#visasTable');
+
+    visasTable.dataTable().fnDestroy();
+
+    var table = visasTable.DataTable({
+        "order": [[ 1, 'desc' ]],
+        data: result,
+        "iDisplayLength": 30, //rows to be shown by default
+        stateSave: true,
+        columnDefs: [
+            {
+                targets: '_all',
+                className: 'dt-body-center'
+            },
+        ],
+        columns: [
+            {
+                "data": "state",
+                title: "Status",
+                "render": function (data, type, row, meta) {
+                    if (type === 'display') {
+                        if (data ==='finished' && row.commentUrl)
+                            data = "<a href='" + row.commentUrl + "'>" + data + "</a>";
+                    }
+
+                    return data;
+                }
+            },
+            {
+                "data": "date",
+                title: "Date"
+            },
+            {
+                "data": "userName",
+                title: "User"
+            },
+            {
+                "data": "branchName",
+                title: "Branch"
+            },
+            {
+                "data": "ticket",
+                title: "Ticket"
+            },
+            {
+                "data": "blockers",
+                title: "Blockers",
+                "render": function (data, type, row, meta) {
+                    if (type === 'display') {
+                        if (row.state != 'finished')
+                            data = "";
+                    }
+
+                    return data;
+                }
+            }
+
+        ]
+    });
+}
+
+function loadData() {
+    $.ajax({
+            url: "rest/visa/history?serverId=apache",
+            success: function (result) {
+                showVisasTable(result);
+            },
+            error: showErrInLoadStatus
+        }
+    );
+}
+</script>
+</body>
+</html>