From b29fafa86b3cfc3a6caf14b7f30a55e49cbcb339 Mon Sep 17 00:00:00 2001
From: Roman Khavronenko <roman@victoriametrics.com>
Date: Fri, 8 Jul 2022 10:26:13 +0200
Subject: [PATCH] vmalert: deprecate alert's status link (#2840)

* vmalert: deprecate alert's status link

Deprecate alert's status link `/api/v1/<groupID>/<alertID>/status` in favour of
`api/v1/alerts?group_id=<group_id>&alert_id=<alert_id>"`.

The change was needed for simplifying logic in vmselect for proxying vmalert's requests.

The old alert's status link will be still supported for a few versions but will be removed in the future.

https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2825
Signed-off-by: hagen1778 <roman@victoriametrics.com>

* vmalert: fix review comments

Signed-off-by: hagen1778 <roman@victoriametrics.com>
---
 app/vmalert/README.md          |   4 +-
 app/vmalert/main.go            |   7 +-
 app/vmalert/main_test.go       |   3 +-
 app/vmalert/tpl/footer.qtpl    |  10 +-
 app/vmalert/tpl/footer.qtpl.go |  54 ++--
 app/vmalert/tpl/header.qtpl    |   9 +-
 app/vmalert/tpl/header.qtpl.go | 127 +++++----
 app/vmalert/utils/links.go     |  12 +
 app/vmalert/web.go             |  78 ++++--
 app/vmalert/web.qtpl           |   8 +-
 app/vmalert/web.qtpl.go        | 498 +++++++++++++++++----------------
 app/vmalert/web_test.go        |  59 +++-
 app/vmalert/web_types.go       |  13 +
 docs/CHANGELOG.md              |   6 +
 docs/vmalert.md                |   4 +-
 15 files changed, 511 insertions(+), 381 deletions(-)
 create mode 100644 app/vmalert/utils/links.go

diff --git a/app/vmalert/README.md b/app/vmalert/README.md
index 3b65798e7b..56722c72d6 100644
--- a/app/vmalert/README.md
+++ b/app/vmalert/README.md
@@ -481,7 +481,7 @@ or time series modification via [relabeling](https://docs.victoriametrics.com/vm
 * `http://<vmalert-addr>` - UI;
 * `http://<vmalert-addr>/api/v1/rules` - list of all loaded groups and rules;
 * `http://<vmalert-addr>/api/v1/alerts` - list of all active alerts;
-* `http://<vmalert-addr>/api/v1/<groupID>/<alertID>/status"` - get alert status by ID.
+* `http://<vmalert-addr>/vmalert/api/v1/alert?group_id=<group_id>&alert_id=<alert_id>"` - get alert status by ID.
   Used as alert source in AlertManager.
 * `http://<vmalert-addr>/metrics` - application metrics.
 * `http://<vmalert-addr>/-/reload` - hot configuration reload.
@@ -681,7 +681,7 @@ The shortlist of configuration flags is the following:
      How often to evaluate the rules (default 1m0s)
   -external.alert.source string
      External Alert Source allows to override the Source link for alerts sent to AlertManager for cases where you want to build a custom link to Grafana, Prometheus or any other service.
-     eg. 'explore?orgId=1&left=[\"now-1h\",\"now\",\"VictoriaMetrics\",{\"expr\": \"{{$expr|quotesEscape|crlfEscape|queryEscape}}\"},{\"mode\":\"Metrics\"},{\"ui\":[true,true,true,\"none\"]}]'.If empty '/api/v1/:groupID/alertID/status' is used
+     eg. 'explore?orgId=1&left=[\"now-1h\",\"now\",\"VictoriaMetrics\",{\"expr\": \"{{$expr|quotesEscape|crlfEscape|queryEscape}}\"},{\"mode\":\"Metrics\"},{\"ui\":[true,true,true,\"none\"]}]'.If empty '/vmalert/api/v1/alert?group_id=&alert_id=' is used
   -external.label array
      Optional label in the form 'Name=value' to add to all generated recording rules and alerts. Pass multiple -label flags in order to add multiple label sets.
      Supports an array of values separated by comma or specified via multiple flags.
diff --git a/app/vmalert/main.go b/app/vmalert/main.go
index eccf4ef3e4..feef1691c0 100644
--- a/app/vmalert/main.go
+++ b/app/vmalert/main.go
@@ -59,7 +59,7 @@ absolute path to all .tpl files in root.`)
 
 	externalURL         = flag.String("external.url", "", "External URL is used as alert's source for sent alerts to the notifier")
 	externalAlertSource = flag.String("external.alert.source", "", `External Alert Source allows to override the Source link for alerts sent to AlertManager for cases where you want to build a custom link to Grafana, Prometheus or any other service.
-eg. 'explore?orgId=1&left=[\"now-1h\",\"now\",\"VictoriaMetrics\",{\"expr\": \"{{$expr|quotesEscape|crlfEscape|queryEscape}}\"},{\"mode\":\"Metrics\"},{\"ui\":[true,true,true,\"none\"]}]'.If empty '/api/v1/:groupID/alertID/status' is used`)
+eg. 'explore?orgId=1&left=[\"now-1h\",\"now\",\"VictoriaMetrics\",{\"expr\": \"{{$expr|quotesEscape|crlfEscape|queryEscape}}\"},{\"mode\":\"Metrics\"},{\"ui\":[true,true,true,\"none\"]}]'.If empty '/vmalert/api/v1/alert?group_id=&alert_id=' is used`)
 	externalLabels = flagutil.NewArray("external.label", "Optional label in the form 'Name=value' to add to all generated recording rules and alerts. "+
 		"Pass multiple -label flags in order to add multiple label sets.")
 
@@ -236,8 +236,9 @@ func getExternalURL(externalURL, httpListenAddr string, isSecure bool) (*url.URL
 
 func getAlertURLGenerator(externalURL *url.URL, externalAlertSource string, validateTemplate bool) (notifier.AlertURLGenerator, error) {
 	if externalAlertSource == "" {
-		return func(alert notifier.Alert) string {
-			return fmt.Sprintf("%s/api/v1/%s/%s/status", externalURL, strconv.FormatUint(alert.GroupID, 10), strconv.FormatUint(alert.ID, 10))
+		return func(a notifier.Alert) string {
+			gID, aID := strconv.FormatUint(a.GroupID, 10), strconv.FormatUint(a.ID, 10)
+			return fmt.Sprintf("%s/vmalert/api/v1/alert?%s=%s&%s=%s", externalURL, paramGroupID, gID, paramAlertID, aID)
 		}, nil
 	}
 	if validateTemplate {
diff --git a/app/vmalert/main_test.go b/app/vmalert/main_test.go
index 4999723ea8..30595d2f02 100644
--- a/app/vmalert/main_test.go
+++ b/app/vmalert/main_test.go
@@ -41,7 +41,8 @@ func TestGetAlertURLGenerator(t *testing.T) {
 	if err != nil {
 		t.Errorf("unexpected error %s", err)
 	}
-	if exp := "https://victoriametrics.com/path/api/v1/42/2/status"; exp != fn(testAlert) {
+	exp := fmt.Sprintf("https://victoriametrics.com/path/vmalert/api/v1/alert?%s=42&%s=2", paramGroupID, paramAlertID)
+	if exp != fn(testAlert) {
 		t.Errorf("unexpected url want %s, got %s", exp, fn(testAlert))
 	}
 	_, err = getAlertURLGenerator(nil, "foo?{{invalid}}", true)
diff --git a/app/vmalert/tpl/footer.qtpl b/app/vmalert/tpl/footer.qtpl
index eef27785d4..df1e3a4ba2 100644
--- a/app/vmalert/tpl/footer.qtpl
+++ b/app/vmalert/tpl/footer.qtpl
@@ -1,16 +1,12 @@
 {% import (
     "net/http"
-    "strings"
+
+    "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
 ) %}
 
 
 {% func Footer(r *http.Request) %}
-{%code
-    prefix := "/vmalert/"
-    if strings.HasPrefix(r.URL.Path, prefix) {
-        prefix = ""
-    }
-%}
+    {%code prefix := utils.Prefix(r.URL.Path) %}
         </main>
         <script src="{%s prefix %}static/js/jquery-3.6.0.min.js" type="text/javascript"></script>
         <script src="{%s prefix %}static/js/bootstrap.bundle.min.js" type="text/javascript"></script>
diff --git a/app/vmalert/tpl/footer.qtpl.go b/app/vmalert/tpl/footer.qtpl.go
index 65eea9fa4b..3f8b098012 100644
--- a/app/vmalert/tpl/footer.qtpl.go
+++ b/app/vmalert/tpl/footer.qtpl.go
@@ -7,45 +7,43 @@ package tpl
 //line app/vmalert/tpl/footer.qtpl:1
 import (
 	"net/http"
-	"strings"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
 )
 
-//line app/vmalert/tpl/footer.qtpl:7
+//line app/vmalert/tpl/footer.qtpl:8
 import (
 	qtio422016 "io"
 
 	qt422016 "github.com/valyala/quicktemplate"
 )
 
-//line app/vmalert/tpl/footer.qtpl:7
+//line app/vmalert/tpl/footer.qtpl:8
 var (
 	_ = qtio422016.Copy
 	_ = qt422016.AcquireByteBuffer
 )
 
-//line app/vmalert/tpl/footer.qtpl:7
+//line app/vmalert/tpl/footer.qtpl:8
 func StreamFooter(qw422016 *qt422016.Writer, r *http.Request) {
-//line app/vmalert/tpl/footer.qtpl:7
+//line app/vmalert/tpl/footer.qtpl:8
 	qw422016.N().S(`
-`)
+    `)
 //line app/vmalert/tpl/footer.qtpl:9
-	prefix := "/vmalert/"
-	if strings.HasPrefix(r.URL.Path, prefix) {
-		prefix = ""
-	}
+	prefix := utils.Prefix(r.URL.Path)
 
-//line app/vmalert/tpl/footer.qtpl:13
+//line app/vmalert/tpl/footer.qtpl:9
 	qw422016.N().S(`
         </main>
         <script src="`)
-//line app/vmalert/tpl/footer.qtpl:15
+//line app/vmalert/tpl/footer.qtpl:11
 	qw422016.E().S(prefix)
-//line app/vmalert/tpl/footer.qtpl:15
+//line app/vmalert/tpl/footer.qtpl:11
 	qw422016.N().S(`static/js/jquery-3.6.0.min.js" type="text/javascript"></script>
         <script src="`)
-//line app/vmalert/tpl/footer.qtpl:16
+//line app/vmalert/tpl/footer.qtpl:12
 	qw422016.E().S(prefix)
-//line app/vmalert/tpl/footer.qtpl:16
+//line app/vmalert/tpl/footer.qtpl:12
 	qw422016.N().S(`static/js/bootstrap.bundle.min.js" type="text/javascript"></script>
         <script type="text/javascript">
             function expandAll() {
@@ -79,31 +77,31 @@ func StreamFooter(qw422016 *qt422016.Writer, r *http.Request) {
     </body>
 </html>
 `)
-//line app/vmalert/tpl/footer.qtpl:48
+//line app/vmalert/tpl/footer.qtpl:44
 }
 
-//line app/vmalert/tpl/footer.qtpl:48
+//line app/vmalert/tpl/footer.qtpl:44
 func WriteFooter(qq422016 qtio422016.Writer, r *http.Request) {
-//line app/vmalert/tpl/footer.qtpl:48
+//line app/vmalert/tpl/footer.qtpl:44
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line app/vmalert/tpl/footer.qtpl:48
+//line app/vmalert/tpl/footer.qtpl:44
 	StreamFooter(qw422016, r)
-//line app/vmalert/tpl/footer.qtpl:48
+//line app/vmalert/tpl/footer.qtpl:44
 	qt422016.ReleaseWriter(qw422016)
-//line app/vmalert/tpl/footer.qtpl:48
+//line app/vmalert/tpl/footer.qtpl:44
 }
 
-//line app/vmalert/tpl/footer.qtpl:48
+//line app/vmalert/tpl/footer.qtpl:44
 func Footer(r *http.Request) string {
-//line app/vmalert/tpl/footer.qtpl:48
+//line app/vmalert/tpl/footer.qtpl:44
 	qb422016 := qt422016.AcquireByteBuffer()
-//line app/vmalert/tpl/footer.qtpl:48
+//line app/vmalert/tpl/footer.qtpl:44
 	WriteFooter(qb422016, r)
-//line app/vmalert/tpl/footer.qtpl:48
+//line app/vmalert/tpl/footer.qtpl:44
 	qs422016 := string(qb422016.B)
-//line app/vmalert/tpl/footer.qtpl:48
+//line app/vmalert/tpl/footer.qtpl:44
 	qt422016.ReleaseByteBuffer(qb422016)
-//line app/vmalert/tpl/footer.qtpl:48
+//line app/vmalert/tpl/footer.qtpl:44
 	return qs422016
-//line app/vmalert/tpl/footer.qtpl:48
+//line app/vmalert/tpl/footer.qtpl:44
 }
diff --git a/app/vmalert/tpl/header.qtpl b/app/vmalert/tpl/header.qtpl
index c999333db4..b5714da293 100644
--- a/app/vmalert/tpl/header.qtpl
+++ b/app/vmalert/tpl/header.qtpl
@@ -2,15 +2,12 @@
     "strings"
     "net/http"
     "path"
+
+    "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
 ) %}
 
 {% func Header(r *http.Request, navItems []NavItem, title string) %}
-{%code
-    prefix := "/vmalert/"
-    if strings.HasPrefix(r.URL.Path, prefix) {
-        prefix = ""
-    }
-%}
+    {%code prefix := utils.Prefix(r.URL.Path) %}
 <!DOCTYPE html>
 <html lang="en">
 <head>
diff --git a/app/vmalert/tpl/header.qtpl.go b/app/vmalert/tpl/header.qtpl.go
index 1c2c55d858..6fb6b0483d 100644
--- a/app/vmalert/tpl/header.qtpl.go
+++ b/app/vmalert/tpl/header.qtpl.go
@@ -9,52 +9,51 @@ import (
 	"net/http"
 	"path"
 	"strings"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
 )
 
-//line app/vmalert/tpl/header.qtpl:7
+//line app/vmalert/tpl/header.qtpl:9
 import (
 	qtio422016 "io"
 
 	qt422016 "github.com/valyala/quicktemplate"
 )
 
-//line app/vmalert/tpl/header.qtpl:7
+//line app/vmalert/tpl/header.qtpl:9
 var (
 	_ = qtio422016.Copy
 	_ = qt422016.AcquireByteBuffer
 )
 
-//line app/vmalert/tpl/header.qtpl:7
-func StreamHeader(qw422016 *qt422016.Writer, r *http.Request, navItems []NavItem, title string) {
-//line app/vmalert/tpl/header.qtpl:7
-	qw422016.N().S(`
-`)
 //line app/vmalert/tpl/header.qtpl:9
-	prefix := "/vmalert/"
-	if strings.HasPrefix(r.URL.Path, prefix) {
-		prefix = ""
-	}
+func StreamHeader(qw422016 *qt422016.Writer, r *http.Request, navItems []NavItem, title string) {
+//line app/vmalert/tpl/header.qtpl:9
+	qw422016.N().S(`
+    `)
+//line app/vmalert/tpl/header.qtpl:10
+	prefix := utils.Prefix(r.URL.Path)
 
-//line app/vmalert/tpl/header.qtpl:13
+//line app/vmalert/tpl/header.qtpl:10
 	qw422016.N().S(`
 <!DOCTYPE html>
 <html lang="en">
 <head>
     <title>vmalert`)
-//line app/vmalert/tpl/header.qtpl:17
+//line app/vmalert/tpl/header.qtpl:14
 	if title != "" {
-//line app/vmalert/tpl/header.qtpl:17
+//line app/vmalert/tpl/header.qtpl:14
 		qw422016.N().S(` - `)
-//line app/vmalert/tpl/header.qtpl:17
+//line app/vmalert/tpl/header.qtpl:14
 		qw422016.E().S(title)
-//line app/vmalert/tpl/header.qtpl:17
+//line app/vmalert/tpl/header.qtpl:14
 	}
-//line app/vmalert/tpl/header.qtpl:17
+//line app/vmalert/tpl/header.qtpl:14
 	qw422016.N().S(`</title>
     <link href="`)
-//line app/vmalert/tpl/header.qtpl:18
+//line app/vmalert/tpl/header.qtpl:15
 	qw422016.E().S(prefix)
-//line app/vmalert/tpl/header.qtpl:18
+//line app/vmalert/tpl/header.qtpl:15
 	qw422016.N().S(`static/css/bootstrap.min.css" rel="stylesheet" />
     <style>
         body{
@@ -105,124 +104,124 @@ func StreamHeader(qw422016 *qt422016.Writer, r *http.Request, navItems []NavItem
 </head>
 <body>
     `)
-//line app/vmalert/tpl/header.qtpl:67
+//line app/vmalert/tpl/header.qtpl:64
 	streamprintNavItems(qw422016, r, title, navItems)
-//line app/vmalert/tpl/header.qtpl:67
+//line app/vmalert/tpl/header.qtpl:64
 	qw422016.N().S(`
     <main class="px-2">
 `)
-//line app/vmalert/tpl/header.qtpl:69
+//line app/vmalert/tpl/header.qtpl:66
 }
 
-//line app/vmalert/tpl/header.qtpl:69
+//line app/vmalert/tpl/header.qtpl:66
 func WriteHeader(qq422016 qtio422016.Writer, r *http.Request, navItems []NavItem, title string) {
-//line app/vmalert/tpl/header.qtpl:69
+//line app/vmalert/tpl/header.qtpl:66
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line app/vmalert/tpl/header.qtpl:69
+//line app/vmalert/tpl/header.qtpl:66
 	StreamHeader(qw422016, r, navItems, title)
-//line app/vmalert/tpl/header.qtpl:69
+//line app/vmalert/tpl/header.qtpl:66
 	qt422016.ReleaseWriter(qw422016)
-//line app/vmalert/tpl/header.qtpl:69
+//line app/vmalert/tpl/header.qtpl:66
 }
 
-//line app/vmalert/tpl/header.qtpl:69
+//line app/vmalert/tpl/header.qtpl:66
 func Header(r *http.Request, navItems []NavItem, title string) string {
-//line app/vmalert/tpl/header.qtpl:69
+//line app/vmalert/tpl/header.qtpl:66
 	qb422016 := qt422016.AcquireByteBuffer()
-//line app/vmalert/tpl/header.qtpl:69
+//line app/vmalert/tpl/header.qtpl:66
 	WriteHeader(qb422016, r, navItems, title)
-//line app/vmalert/tpl/header.qtpl:69
+//line app/vmalert/tpl/header.qtpl:66
 	qs422016 := string(qb422016.B)
-//line app/vmalert/tpl/header.qtpl:69
+//line app/vmalert/tpl/header.qtpl:66
 	qt422016.ReleaseByteBuffer(qb422016)
-//line app/vmalert/tpl/header.qtpl:69
+//line app/vmalert/tpl/header.qtpl:66
 	return qs422016
-//line app/vmalert/tpl/header.qtpl:69
+//line app/vmalert/tpl/header.qtpl:66
 }
 
-//line app/vmalert/tpl/header.qtpl:73
+//line app/vmalert/tpl/header.qtpl:70
 type NavItem struct {
 	Name string
 	Url  string
 }
 
-//line app/vmalert/tpl/header.qtpl:79
+//line app/vmalert/tpl/header.qtpl:76
 func streamprintNavItems(qw422016 *qt422016.Writer, r *http.Request, current string, items []NavItem) {
-//line app/vmalert/tpl/header.qtpl:79
+//line app/vmalert/tpl/header.qtpl:76
 	qw422016.N().S(`
 `)
-//line app/vmalert/tpl/header.qtpl:81
+//line app/vmalert/tpl/header.qtpl:78
 	prefix := "/vmalert/"
 	if strings.HasPrefix(r.URL.Path, prefix) {
 		prefix = ""
 	}
 
-//line app/vmalert/tpl/header.qtpl:85
+//line app/vmalert/tpl/header.qtpl:82
 	qw422016.N().S(`
 <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
   <div class="container-fluid">
     <div class="collapse navbar-collapse" id="navbarCollapse">
         <ul class="navbar-nav me-auto mb-2 mb-md-0">
             `)
-//line app/vmalert/tpl/header.qtpl:90
+//line app/vmalert/tpl/header.qtpl:87
 	for _, item := range items {
-//line app/vmalert/tpl/header.qtpl:90
+//line app/vmalert/tpl/header.qtpl:87
 		qw422016.N().S(`
                 <li class="nav-item">
                     <a class="nav-link`)
-//line app/vmalert/tpl/header.qtpl:92
+//line app/vmalert/tpl/header.qtpl:89
 		if current == item.Name {
-//line app/vmalert/tpl/header.qtpl:92
+//line app/vmalert/tpl/header.qtpl:89
 			qw422016.N().S(` active`)
-//line app/vmalert/tpl/header.qtpl:92
+//line app/vmalert/tpl/header.qtpl:89
 		}
-//line app/vmalert/tpl/header.qtpl:92
+//line app/vmalert/tpl/header.qtpl:89
 		qw422016.N().S(`" href="`)
-//line app/vmalert/tpl/header.qtpl:92
+//line app/vmalert/tpl/header.qtpl:89
 		qw422016.E().S(path.Join(prefix, item.Url))
-//line app/vmalert/tpl/header.qtpl:92
+//line app/vmalert/tpl/header.qtpl:89
 		qw422016.N().S(`">
                         `)
-//line app/vmalert/tpl/header.qtpl:93
+//line app/vmalert/tpl/header.qtpl:90
 		qw422016.E().S(item.Name)
-//line app/vmalert/tpl/header.qtpl:93
+//line app/vmalert/tpl/header.qtpl:90
 		qw422016.N().S(`
                     </a>
                 </li>
             `)
-//line app/vmalert/tpl/header.qtpl:96
+//line app/vmalert/tpl/header.qtpl:93
 	}
-//line app/vmalert/tpl/header.qtpl:96
+//line app/vmalert/tpl/header.qtpl:93
 	qw422016.N().S(`
         </ul>
   </div>
 </nav>
 `)
-//line app/vmalert/tpl/header.qtpl:100
+//line app/vmalert/tpl/header.qtpl:97
 }
 
-//line app/vmalert/tpl/header.qtpl:100
+//line app/vmalert/tpl/header.qtpl:97
 func writeprintNavItems(qq422016 qtio422016.Writer, r *http.Request, current string, items []NavItem) {
-//line app/vmalert/tpl/header.qtpl:100
+//line app/vmalert/tpl/header.qtpl:97
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line app/vmalert/tpl/header.qtpl:100
+//line app/vmalert/tpl/header.qtpl:97
 	streamprintNavItems(qw422016, r, current, items)
-//line app/vmalert/tpl/header.qtpl:100
+//line app/vmalert/tpl/header.qtpl:97
 	qt422016.ReleaseWriter(qw422016)
-//line app/vmalert/tpl/header.qtpl:100
+//line app/vmalert/tpl/header.qtpl:97
 }
 
-//line app/vmalert/tpl/header.qtpl:100
+//line app/vmalert/tpl/header.qtpl:97
 func printNavItems(r *http.Request, current string, items []NavItem) string {
-//line app/vmalert/tpl/header.qtpl:100
+//line app/vmalert/tpl/header.qtpl:97
 	qb422016 := qt422016.AcquireByteBuffer()
-//line app/vmalert/tpl/header.qtpl:100
+//line app/vmalert/tpl/header.qtpl:97
 	writeprintNavItems(qb422016, r, current, items)
-//line app/vmalert/tpl/header.qtpl:100
+//line app/vmalert/tpl/header.qtpl:97
 	qs422016 := string(qb422016.B)
-//line app/vmalert/tpl/header.qtpl:100
+//line app/vmalert/tpl/header.qtpl:97
 	qt422016.ReleaseByteBuffer(qb422016)
-//line app/vmalert/tpl/header.qtpl:100
+//line app/vmalert/tpl/header.qtpl:97
 	return qs422016
-//line app/vmalert/tpl/header.qtpl:100
+//line app/vmalert/tpl/header.qtpl:97
 }
diff --git a/app/vmalert/utils/links.go b/app/vmalert/utils/links.go
new file mode 100644
index 0000000000..00a9d3a62e
--- /dev/null
+++ b/app/vmalert/utils/links.go
@@ -0,0 +1,12 @@
+package utils
+
+import "strings"
+
+const prefix = "/vmalert/"
+
+func Prefix(path string) string {
+	if strings.HasPrefix(path, prefix) {
+		return ""
+	}
+	return prefix
+}
diff --git a/app/vmalert/web.go b/app/vmalert/web.go
index 1969db7507..ae9c156167 100644
--- a/app/vmalert/web.go
+++ b/app/vmalert/web.go
@@ -25,11 +25,11 @@ var (
 
 func initLinks() {
 	apiLinks = [][2]string{
-		// api links are relative since they can be used by external clients
-		// such as Grafana and proxied via vmselect.
+		// api links are relative since they can be used by external clients,
+		// such as Grafana, and proxied via vmselect.
 		{"api/v1/rules", "list all loaded groups and rules"},
 		{"api/v1/alerts", "list all active alerts"},
-		{"api/v1/groupID/alertID/status", "get alert status by ID"},
+		{fmt.Sprintf("api/v1/alert?%s=<int>&%s=<int>", paramGroupID, paramAlertID), "get alert status by group and alert ID"},
 
 		// system links
 		{"/flags", "command-line flags"},
@@ -76,6 +76,14 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
 	case "/vmalert/alerts":
 		WriteListAlerts(w, r, rh.groupAlerts())
 		return true
+	case "/vmalert/alert":
+		alert, err := rh.getAlert(r)
+		if err != nil {
+			httpserver.Errorf(w, r, "%s", err)
+			return true
+		}
+		WriteAlert(w, r, alert)
+		return true
 	case "/vmalert/groups":
 		WriteListGroups(w, r, rh.groups())
 		return true
@@ -111,7 +119,20 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
 		w.Header().Set("Content-Type", "application/json")
 		w.Write(data)
 		return true
-
+	case "/vmalert/api/v1/alert", "/api/v1/alert":
+		alert, err := rh.getAlert(r)
+		if err != nil {
+			httpserver.Errorf(w, r, "%s", err)
+			return true
+		}
+		data, err := json.Marshal(alert)
+		if err != nil {
+			httpserver.Errorf(w, r, "failed to marshal alert: %s", err)
+			return true
+		}
+		w.Header().Set("Content-Type", "application/json")
+		w.Write(data)
+		return true
 	case "/-/reload":
 		logger.Infof("api config reload was called, sending sighup")
 		procutil.SelfSIGHUP()
@@ -119,6 +140,11 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
 		return true
 
 	default:
+		// Support of deprecated links:
+		// * /api/v1/<groupID>/<alertID>/status
+		// * <groupID>/<alertID>/status
+		// TODO: to remove in next versions
+
 		if !strings.HasSuffix(r.URL.Path, "/status") {
 			return false
 		}
@@ -128,24 +154,36 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
 			return true
 		}
 
-		// /api/v1/<groupID>/<alertID>/status
+		redirectURL := alert.WebLink()
 		if strings.HasPrefix(r.URL.Path, "/api/v1/") {
-			data, err := json.Marshal(alert)
-			if err != nil {
-				httpserver.Errorf(w, r, "failed to marshal alert: %s", err)
-				return true
-			}
-			w.Header().Set("Content-Type", "application/json")
-			w.Write(data)
-			return true
+			redirectURL = alert.APILink()
 		}
-
-		// <groupID>/<alertID>/status
-		WriteAlert(w, r, alert)
+		http.Redirect(w, r, "/"+redirectURL, http.StatusPermanentRedirect)
 		return true
 	}
 }
 
+const (
+	paramGroupID = "group_id"
+	paramAlertID = "alert_id"
+)
+
+func (rh *requestHandler) getAlert(r *http.Request) (*APIAlert, error) {
+	groupID, err := strconv.ParseUint(r.FormValue(paramGroupID), 10, 0)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read %q param: %s", paramGroupID, err)
+	}
+	alertID, err := strconv.ParseUint(r.FormValue(paramAlertID), 10, 0)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read %q param: %s", paramAlertID, err)
+	}
+	a, err := rh.m.AlertAPI(groupID, alertID)
+	if err != nil {
+		return nil, errResponse(err, http.StatusNotFound)
+	}
+	return a, nil
+}
+
 type listGroupsResponse struct {
 	Status string `json:"status"`
 	Data   struct {
@@ -245,10 +283,10 @@ func (rh *requestHandler) listAlerts() ([]byte, error) {
 }
 
 func (rh *requestHandler) alertByPath(path string) (*APIAlert, error) {
-	rh.m.groupsMu.RLock()
-	defer rh.m.groupsMu.RUnlock()
-
-	parts := strings.SplitN(strings.TrimLeft(path, "/"), "/", 3)
+	if strings.HasPrefix(path, "/vmalert") {
+		path = strings.TrimLeft(path, "/vmalert")
+	}
+	parts := strings.SplitN(strings.TrimLeft(path, "/"), "/", -1)
 	if len(parts) != 3 {
 		return nil, &httpserver.ErrorWithStatusCode{
 			Err:        fmt.Errorf(`path %q cointains /status suffix but doesn't match pattern "/groupID/alertID/status"`, path),
diff --git a/app/vmalert/web.qtpl b/app/vmalert/web.qtpl
index d5e5c316f4..7e5db0a5c9 100644
--- a/app/vmalert/web.qtpl
+++ b/app/vmalert/web.qtpl
@@ -3,10 +3,10 @@
 {% import (
     "time"
     "sort"
-    "path"
     "net/http"
 
     "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/tpl"
+    "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
     "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
 ) %}
 
@@ -119,6 +119,7 @@
 
 
 {% func ListAlerts(r *http.Request, groupAlerts []GroupAlerts) %}
+    {%code prefix := utils.Prefix(r.URL.Path) %}
     {%= tpl.Header(r, navItems, "Alerts") %}
     {% if len(groupAlerts) > 0 %}
          <a class="btn btn-primary" role="button" onclick="collapseAll()">Collapse All</a>
@@ -183,7 +184,7 @@
                                 </td>
                                 <td>{%s ar.Value %}</td>
                                 <td>
-                                    <a href="{%s path.Join(g.ID, ar.ID, "status") %}">Details</a>
+                                    <a href="{%s prefix+ar.WebLink() %}">Details</a>
                                 </td>
                             </tr>
                         {% endfor %}
@@ -261,6 +262,7 @@
 {% endfunc %}
 
 {% func Alert(r *http.Request, alert *APIAlert) %}
+    {%code prefix := utils.Prefix(r.URL.Path) %}
     {%= tpl.Header(r, navItems, "") %}
     {%code
         var labelKeys []string
@@ -327,7 +329,7 @@
           Group
         </div>
         <div class="col">
-           <a target="_blank" href="/groups#group-{%s alert.GroupID %}">{%s alert.GroupID %}</a>
+           <a target="_blank" href="{%s prefix %}groups#group-{%s alert.GroupID %}">{%s alert.GroupID %}</a>
         </div>
       </div>
     </div>
diff --git a/app/vmalert/web.qtpl.go b/app/vmalert/web.qtpl.go
index d37e9d2e30..aa73307c6f 100644
--- a/app/vmalert/web.qtpl.go
+++ b/app/vmalert/web.qtpl.go
@@ -7,12 +7,12 @@ package main
 //line app/vmalert/web.qtpl:3
 import (
 	"net/http"
-	"path"
 	"sort"
 	"time"
 
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/tpl"
+	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
 )
 
 //line app/vmalert/web.qtpl:14
@@ -434,70 +434,76 @@ func StreamListAlerts(qw422016 *qt422016.Writer, r *http.Request, groupAlerts []
 	qw422016.N().S(`
     `)
 //line app/vmalert/web.qtpl:122
-	tpl.StreamHeader(qw422016, r, navItems, "Alerts")
+	prefix := utils.Prefix(r.URL.Path)
+
 //line app/vmalert/web.qtpl:122
 	qw422016.N().S(`
     `)
 //line app/vmalert/web.qtpl:123
-	if len(groupAlerts) > 0 {
+	tpl.StreamHeader(qw422016, r, navItems, "Alerts")
 //line app/vmalert/web.qtpl:123
+	qw422016.N().S(`
+    `)
+//line app/vmalert/web.qtpl:124
+	if len(groupAlerts) > 0 {
+//line app/vmalert/web.qtpl:124
 		qw422016.N().S(`
          <a class="btn btn-primary" role="button" onclick="collapseAll()">Collapse All</a>
          <a class="btn btn-primary" role="button" onclick="expandAll()">Expand All</a>
          `)
-//line app/vmalert/web.qtpl:126
+//line app/vmalert/web.qtpl:127
 		for _, ga := range groupAlerts {
-//line app/vmalert/web.qtpl:126
+//line app/vmalert/web.qtpl:127
 			qw422016.N().S(`
             `)
-//line app/vmalert/web.qtpl:127
+//line app/vmalert/web.qtpl:128
 			g := ga.Group
 
-//line app/vmalert/web.qtpl:127
+//line app/vmalert/web.qtpl:128
 			qw422016.N().S(`
             <div class="group-heading alert-danger" data-bs-target="rules-`)
-//line app/vmalert/web.qtpl:128
+//line app/vmalert/web.qtpl:129
 			qw422016.E().S(g.ID)
-//line app/vmalert/web.qtpl:128
+//line app/vmalert/web.qtpl:129
 			qw422016.N().S(`">
                 <span class="anchor" id="group-`)
-//line app/vmalert/web.qtpl:129
+//line app/vmalert/web.qtpl:130
 			qw422016.E().S(g.ID)
-//line app/vmalert/web.qtpl:129
+//line app/vmalert/web.qtpl:130
 			qw422016.N().S(`"></span>
                 <a href="#group-`)
-//line app/vmalert/web.qtpl:130
+//line app/vmalert/web.qtpl:131
 			qw422016.E().S(g.ID)
-//line app/vmalert/web.qtpl:130
+//line app/vmalert/web.qtpl:131
 			qw422016.N().S(`">`)
-//line app/vmalert/web.qtpl:130
+//line app/vmalert/web.qtpl:131
 			qw422016.E().S(g.Name)
-//line app/vmalert/web.qtpl:130
+//line app/vmalert/web.qtpl:131
 			if g.Type != "prometheus" {
-//line app/vmalert/web.qtpl:130
+//line app/vmalert/web.qtpl:131
 				qw422016.N().S(` (`)
-//line app/vmalert/web.qtpl:130
+//line app/vmalert/web.qtpl:131
 				qw422016.E().S(g.Type)
-//line app/vmalert/web.qtpl:130
+//line app/vmalert/web.qtpl:131
 				qw422016.N().S(`)`)
-//line app/vmalert/web.qtpl:130
+//line app/vmalert/web.qtpl:131
 			}
-//line app/vmalert/web.qtpl:130
+//line app/vmalert/web.qtpl:131
 			qw422016.N().S(`</a>
                 <span class="badge bg-danger" title="Number of active alerts">`)
-//line app/vmalert/web.qtpl:131
+//line app/vmalert/web.qtpl:132
 			qw422016.N().D(len(ga.Alerts))
-//line app/vmalert/web.qtpl:131
+//line app/vmalert/web.qtpl:132
 			qw422016.N().S(`</span>
                 <br>
                 <p class="fs-6 fw-lighter">`)
-//line app/vmalert/web.qtpl:133
+//line app/vmalert/web.qtpl:134
 			qw422016.E().S(g.File)
-//line app/vmalert/web.qtpl:133
+//line app/vmalert/web.qtpl:134
 			qw422016.N().S(`</p>
             </div>
             `)
-//line app/vmalert/web.qtpl:136
+//line app/vmalert/web.qtpl:137
 			var keys []string
 			alertsByRule := make(map[string][]*APIAlert)
 			for _, alert := range ga.Alerts {
@@ -508,20 +514,20 @@ func StreamListAlerts(qw422016 *qt422016.Writer, r *http.Request, groupAlerts []
 			}
 			sort.Strings(keys)
 
-//line app/vmalert/web.qtpl:145
+//line app/vmalert/web.qtpl:146
 			qw422016.N().S(`
             <div class="collapse" id="rules-`)
-//line app/vmalert/web.qtpl:146
+//line app/vmalert/web.qtpl:147
 			qw422016.E().S(g.ID)
-//line app/vmalert/web.qtpl:146
+//line app/vmalert/web.qtpl:147
 			qw422016.N().S(`">
                 `)
-//line app/vmalert/web.qtpl:147
+//line app/vmalert/web.qtpl:148
 			for _, ruleID := range keys {
-//line app/vmalert/web.qtpl:147
+//line app/vmalert/web.qtpl:148
 				qw422016.N().S(`
                     `)
-//line app/vmalert/web.qtpl:149
+//line app/vmalert/web.qtpl:150
 				defaultAR := alertsByRule[ruleID][0]
 				var labelKeys []string
 				for k := range defaultAR.Labels {
@@ -529,28 +535,28 @@ func StreamListAlerts(qw422016 *qt422016.Writer, r *http.Request, groupAlerts []
 				}
 				sort.Strings(labelKeys)
 
-//line app/vmalert/web.qtpl:155
+//line app/vmalert/web.qtpl:156
 				qw422016.N().S(`
                     <br>
                     <b>alert:</b> `)
-//line app/vmalert/web.qtpl:157
+//line app/vmalert/web.qtpl:158
 				qw422016.E().S(defaultAR.Name)
-//line app/vmalert/web.qtpl:157
+//line app/vmalert/web.qtpl:158
 				qw422016.N().S(` (`)
-//line app/vmalert/web.qtpl:157
+//line app/vmalert/web.qtpl:158
 				qw422016.N().D(len(alertsByRule[ruleID]))
-//line app/vmalert/web.qtpl:157
+//line app/vmalert/web.qtpl:158
 				qw422016.N().S(`)
                      | <span><a target="_blank" href="`)
-//line app/vmalert/web.qtpl:158
+//line app/vmalert/web.qtpl:159
 				qw422016.E().S(defaultAR.SourceLink)
-//line app/vmalert/web.qtpl:158
+//line app/vmalert/web.qtpl:159
 				qw422016.N().S(`">Source</a></span>
                     <br>
                     <b>expr:</b><code><pre>`)
-//line app/vmalert/web.qtpl:160
+//line app/vmalert/web.qtpl:161
 				qw422016.E().S(defaultAR.Expression)
-//line app/vmalert/web.qtpl:160
+//line app/vmalert/web.qtpl:161
 				qw422016.N().S(`</pre></code>
                     <table class="table table-striped table-hover table-sm">
                         <thead>
@@ -564,204 +570,204 @@ func StreamListAlerts(qw422016 *qt422016.Writer, r *http.Request, groupAlerts []
                         </thead>
                         <tbody>
                         `)
-//line app/vmalert/web.qtpl:172
+//line app/vmalert/web.qtpl:173
 				for _, ar := range alertsByRule[ruleID] {
-//line app/vmalert/web.qtpl:172
+//line app/vmalert/web.qtpl:173
 					qw422016.N().S(`
                             <tr>
                                 <td>
                                     `)
-//line app/vmalert/web.qtpl:175
+//line app/vmalert/web.qtpl:176
 					for _, k := range labelKeys {
-//line app/vmalert/web.qtpl:175
+//line app/vmalert/web.qtpl:176
 						qw422016.N().S(`
                                         <span class="ms-1 badge bg-primary">`)
-//line app/vmalert/web.qtpl:176
+//line app/vmalert/web.qtpl:177
 						qw422016.E().S(k)
-//line app/vmalert/web.qtpl:176
+//line app/vmalert/web.qtpl:177
 						qw422016.N().S(`=`)
-//line app/vmalert/web.qtpl:176
+//line app/vmalert/web.qtpl:177
 						qw422016.E().S(ar.Labels[k])
-//line app/vmalert/web.qtpl:176
+//line app/vmalert/web.qtpl:177
 						qw422016.N().S(`</span>
                                     `)
-//line app/vmalert/web.qtpl:177
+//line app/vmalert/web.qtpl:178
 					}
-//line app/vmalert/web.qtpl:177
+//line app/vmalert/web.qtpl:178
 					qw422016.N().S(`
                                 </td>
                                 <td>`)
-//line app/vmalert/web.qtpl:179
+//line app/vmalert/web.qtpl:180
 					streambadgeState(qw422016, ar.State)
-//line app/vmalert/web.qtpl:179
+//line app/vmalert/web.qtpl:180
 					qw422016.N().S(`</td>
                                 <td>
                                     `)
-//line app/vmalert/web.qtpl:181
+//line app/vmalert/web.qtpl:182
 					qw422016.E().S(ar.ActiveAt.Format("2006-01-02T15:04:05Z07:00"))
-//line app/vmalert/web.qtpl:181
+//line app/vmalert/web.qtpl:182
 					qw422016.N().S(`
                                     `)
-//line app/vmalert/web.qtpl:182
+//line app/vmalert/web.qtpl:183
 					if ar.Restored {
-//line app/vmalert/web.qtpl:182
+//line app/vmalert/web.qtpl:183
 						streambadgeRestored(qw422016)
-//line app/vmalert/web.qtpl:182
+//line app/vmalert/web.qtpl:183
 					}
-//line app/vmalert/web.qtpl:182
+//line app/vmalert/web.qtpl:183
 					qw422016.N().S(`
                                 </td>
                                 <td>`)
-//line app/vmalert/web.qtpl:184
+//line app/vmalert/web.qtpl:185
 					qw422016.E().S(ar.Value)
-//line app/vmalert/web.qtpl:184
+//line app/vmalert/web.qtpl:185
 					qw422016.N().S(`</td>
                                 <td>
                                     <a href="`)
-//line app/vmalert/web.qtpl:186
-					qw422016.E().S(path.Join(g.ID, ar.ID, "status"))
-//line app/vmalert/web.qtpl:186
+//line app/vmalert/web.qtpl:187
+					qw422016.E().S(prefix + ar.WebLink())
+//line app/vmalert/web.qtpl:187
 					qw422016.N().S(`">Details</a>
                                 </td>
                             </tr>
                         `)
-//line app/vmalert/web.qtpl:189
+//line app/vmalert/web.qtpl:190
 				}
-//line app/vmalert/web.qtpl:189
+//line app/vmalert/web.qtpl:190
 				qw422016.N().S(`
                      </tbody>
                     </table>
                 `)
-//line app/vmalert/web.qtpl:192
+//line app/vmalert/web.qtpl:193
 			}
-//line app/vmalert/web.qtpl:192
+//line app/vmalert/web.qtpl:193
 			qw422016.N().S(`
             </div>
             <br>
         `)
-//line app/vmalert/web.qtpl:195
+//line app/vmalert/web.qtpl:196
 		}
-//line app/vmalert/web.qtpl:195
+//line app/vmalert/web.qtpl:196
 		qw422016.N().S(`
 
     `)
-//line app/vmalert/web.qtpl:197
+//line app/vmalert/web.qtpl:198
 	} else {
-//line app/vmalert/web.qtpl:197
+//line app/vmalert/web.qtpl:198
 		qw422016.N().S(`
         <div>
             <p>No items...</p>
         </div>
     `)
-//line app/vmalert/web.qtpl:201
+//line app/vmalert/web.qtpl:202
 	}
-//line app/vmalert/web.qtpl:201
+//line app/vmalert/web.qtpl:202
 	qw422016.N().S(`
 
     `)
-//line app/vmalert/web.qtpl:203
+//line app/vmalert/web.qtpl:204
 	tpl.StreamFooter(qw422016, r)
-//line app/vmalert/web.qtpl:203
+//line app/vmalert/web.qtpl:204
 	qw422016.N().S(`
 
 `)
-//line app/vmalert/web.qtpl:205
+//line app/vmalert/web.qtpl:206
 }
 
-//line app/vmalert/web.qtpl:205
+//line app/vmalert/web.qtpl:206
 func WriteListAlerts(qq422016 qtio422016.Writer, r *http.Request, groupAlerts []GroupAlerts) {
-//line app/vmalert/web.qtpl:205
+//line app/vmalert/web.qtpl:206
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line app/vmalert/web.qtpl:205
+//line app/vmalert/web.qtpl:206
 	StreamListAlerts(qw422016, r, groupAlerts)
-//line app/vmalert/web.qtpl:205
+//line app/vmalert/web.qtpl:206
 	qt422016.ReleaseWriter(qw422016)
-//line app/vmalert/web.qtpl:205
+//line app/vmalert/web.qtpl:206
 }
 
-//line app/vmalert/web.qtpl:205
+//line app/vmalert/web.qtpl:206
 func ListAlerts(r *http.Request, groupAlerts []GroupAlerts) string {
-//line app/vmalert/web.qtpl:205
+//line app/vmalert/web.qtpl:206
 	qb422016 := qt422016.AcquireByteBuffer()
-//line app/vmalert/web.qtpl:205
+//line app/vmalert/web.qtpl:206
 	WriteListAlerts(qb422016, r, groupAlerts)
-//line app/vmalert/web.qtpl:205
+//line app/vmalert/web.qtpl:206
 	qs422016 := string(qb422016.B)
-//line app/vmalert/web.qtpl:205
+//line app/vmalert/web.qtpl:206
 	qt422016.ReleaseByteBuffer(qb422016)
-//line app/vmalert/web.qtpl:205
+//line app/vmalert/web.qtpl:206
 	return qs422016
-//line app/vmalert/web.qtpl:205
+//line app/vmalert/web.qtpl:206
 }
 
-//line app/vmalert/web.qtpl:207
+//line app/vmalert/web.qtpl:208
 func StreamListTargets(qw422016 *qt422016.Writer, r *http.Request, targets map[notifier.TargetType][]notifier.Target) {
-//line app/vmalert/web.qtpl:207
+//line app/vmalert/web.qtpl:208
 	qw422016.N().S(`
     `)
-//line app/vmalert/web.qtpl:208
+//line app/vmalert/web.qtpl:209
 	tpl.StreamHeader(qw422016, r, navItems, "Notifiers")
-//line app/vmalert/web.qtpl:208
+//line app/vmalert/web.qtpl:209
 	qw422016.N().S(`
     `)
-//line app/vmalert/web.qtpl:209
+//line app/vmalert/web.qtpl:210
 	if len(targets) > 0 {
-//line app/vmalert/web.qtpl:209
+//line app/vmalert/web.qtpl:210
 		qw422016.N().S(`
          <a class="btn btn-primary" role="button" onclick="collapseAll()">Collapse All</a>
          <a class="btn btn-primary" role="button" onclick="expandAll()">Expand All</a>
 
          `)
-//line app/vmalert/web.qtpl:214
+//line app/vmalert/web.qtpl:215
 		var keys []string
 		for key := range targets {
 			keys = append(keys, string(key))
 		}
 		sort.Strings(keys)
 
-//line app/vmalert/web.qtpl:219
+//line app/vmalert/web.qtpl:220
 		qw422016.N().S(`
 
          `)
-//line app/vmalert/web.qtpl:221
+//line app/vmalert/web.qtpl:222
 		for i := range keys {
-//line app/vmalert/web.qtpl:221
+//line app/vmalert/web.qtpl:222
 			qw422016.N().S(`
            `)
-//line app/vmalert/web.qtpl:222
+//line app/vmalert/web.qtpl:223
 			typeK, ns := keys[i], targets[notifier.TargetType(keys[i])]
 			count := len(ns)
 
-//line app/vmalert/web.qtpl:224
+//line app/vmalert/web.qtpl:225
 			qw422016.N().S(`
            <div class="group-heading data-bs-target="rules-`)
-//line app/vmalert/web.qtpl:225
+//line app/vmalert/web.qtpl:226
 			qw422016.E().S(typeK)
-//line app/vmalert/web.qtpl:225
+//line app/vmalert/web.qtpl:226
 			qw422016.N().S(`">
              <span class="anchor" id="notifiers-`)
-//line app/vmalert/web.qtpl:226
+//line app/vmalert/web.qtpl:227
 			qw422016.E().S(typeK)
-//line app/vmalert/web.qtpl:226
+//line app/vmalert/web.qtpl:227
 			qw422016.N().S(`"></span>
              <a href="#notifiers-`)
-//line app/vmalert/web.qtpl:227
+//line app/vmalert/web.qtpl:228
 			qw422016.E().S(typeK)
-//line app/vmalert/web.qtpl:227
+//line app/vmalert/web.qtpl:228
 			qw422016.N().S(`">`)
-//line app/vmalert/web.qtpl:227
+//line app/vmalert/web.qtpl:228
 			qw422016.E().S(typeK)
-//line app/vmalert/web.qtpl:227
+//line app/vmalert/web.qtpl:228
 			qw422016.N().S(` (`)
-//line app/vmalert/web.qtpl:227
+//line app/vmalert/web.qtpl:228
 			qw422016.N().D(count)
-//line app/vmalert/web.qtpl:227
+//line app/vmalert/web.qtpl:228
 			qw422016.N().S(`)</a>
          </div>
          <div class="collapse show" id="notifiers-`)
-//line app/vmalert/web.qtpl:229
+//line app/vmalert/web.qtpl:230
 			qw422016.E().S(typeK)
-//line app/vmalert/web.qtpl:229
+//line app/vmalert/web.qtpl:230
 			qw422016.N().S(`">
              <table class="table table-striped table-hover table-sm">
                  <thead>
@@ -772,113 +778,119 @@ func StreamListTargets(qw422016 *qt422016.Writer, r *http.Request, targets map[n
                  </thead>
                  <tbody>
                  `)
-//line app/vmalert/web.qtpl:238
+//line app/vmalert/web.qtpl:239
 			for _, n := range ns {
-//line app/vmalert/web.qtpl:238
+//line app/vmalert/web.qtpl:239
 				qw422016.N().S(`
                      <tr>
                          <td>
                               `)
-//line app/vmalert/web.qtpl:241
+//line app/vmalert/web.qtpl:242
 				for _, l := range n.Labels {
-//line app/vmalert/web.qtpl:241
+//line app/vmalert/web.qtpl:242
 					qw422016.N().S(`
                                       <span class="ms-1 badge bg-primary">`)
-//line app/vmalert/web.qtpl:242
+//line app/vmalert/web.qtpl:243
 					qw422016.E().S(l.Name)
-//line app/vmalert/web.qtpl:242
+//line app/vmalert/web.qtpl:243
 					qw422016.N().S(`=`)
-//line app/vmalert/web.qtpl:242
+//line app/vmalert/web.qtpl:243
 					qw422016.E().S(l.Value)
-//line app/vmalert/web.qtpl:242
+//line app/vmalert/web.qtpl:243
 					qw422016.N().S(`</span>
                               `)
-//line app/vmalert/web.qtpl:243
+//line app/vmalert/web.qtpl:244
 				}
-//line app/vmalert/web.qtpl:243
+//line app/vmalert/web.qtpl:244
 				qw422016.N().S(`
                           </td>
                          <td>`)
-//line app/vmalert/web.qtpl:245
+//line app/vmalert/web.qtpl:246
 				qw422016.E().S(n.Notifier.Addr())
-//line app/vmalert/web.qtpl:245
+//line app/vmalert/web.qtpl:246
 				qw422016.N().S(`</td>
                      </tr>
                  `)
-//line app/vmalert/web.qtpl:247
+//line app/vmalert/web.qtpl:248
 			}
-//line app/vmalert/web.qtpl:247
+//line app/vmalert/web.qtpl:248
 			qw422016.N().S(`
               </tbody>
              </table>
          </div>
      `)
-//line app/vmalert/web.qtpl:251
+//line app/vmalert/web.qtpl:252
 		}
-//line app/vmalert/web.qtpl:251
+//line app/vmalert/web.qtpl:252
 		qw422016.N().S(`
 
     `)
-//line app/vmalert/web.qtpl:253
+//line app/vmalert/web.qtpl:254
 	} else {
-//line app/vmalert/web.qtpl:253
+//line app/vmalert/web.qtpl:254
 		qw422016.N().S(`
         <div>
             <p>No items...</p>
         </div>
     `)
-//line app/vmalert/web.qtpl:257
+//line app/vmalert/web.qtpl:258
 	}
-//line app/vmalert/web.qtpl:257
+//line app/vmalert/web.qtpl:258
 	qw422016.N().S(`
 
     `)
-//line app/vmalert/web.qtpl:259
+//line app/vmalert/web.qtpl:260
 	tpl.StreamFooter(qw422016, r)
-//line app/vmalert/web.qtpl:259
+//line app/vmalert/web.qtpl:260
 	qw422016.N().S(`
 
 `)
-//line app/vmalert/web.qtpl:261
+//line app/vmalert/web.qtpl:262
 }
 
-//line app/vmalert/web.qtpl:261
+//line app/vmalert/web.qtpl:262
 func WriteListTargets(qq422016 qtio422016.Writer, r *http.Request, targets map[notifier.TargetType][]notifier.Target) {
-//line app/vmalert/web.qtpl:261
+//line app/vmalert/web.qtpl:262
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line app/vmalert/web.qtpl:261
+//line app/vmalert/web.qtpl:262
 	StreamListTargets(qw422016, r, targets)
-//line app/vmalert/web.qtpl:261
+//line app/vmalert/web.qtpl:262
 	qt422016.ReleaseWriter(qw422016)
-//line app/vmalert/web.qtpl:261
+//line app/vmalert/web.qtpl:262
 }
 
-//line app/vmalert/web.qtpl:261
+//line app/vmalert/web.qtpl:262
 func ListTargets(r *http.Request, targets map[notifier.TargetType][]notifier.Target) string {
-//line app/vmalert/web.qtpl:261
+//line app/vmalert/web.qtpl:262
 	qb422016 := qt422016.AcquireByteBuffer()
-//line app/vmalert/web.qtpl:261
+//line app/vmalert/web.qtpl:262
 	WriteListTargets(qb422016, r, targets)
-//line app/vmalert/web.qtpl:261
+//line app/vmalert/web.qtpl:262
 	qs422016 := string(qb422016.B)
-//line app/vmalert/web.qtpl:261
+//line app/vmalert/web.qtpl:262
 	qt422016.ReleaseByteBuffer(qb422016)
-//line app/vmalert/web.qtpl:261
+//line app/vmalert/web.qtpl:262
 	return qs422016
-//line app/vmalert/web.qtpl:261
+//line app/vmalert/web.qtpl:262
 }
 
-//line app/vmalert/web.qtpl:263
+//line app/vmalert/web.qtpl:264
 func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) {
-//line app/vmalert/web.qtpl:263
+//line app/vmalert/web.qtpl:264
 	qw422016.N().S(`
     `)
-//line app/vmalert/web.qtpl:264
-	tpl.StreamHeader(qw422016, r, navItems, "")
-//line app/vmalert/web.qtpl:264
+//line app/vmalert/web.qtpl:265
+	prefix := utils.Prefix(r.URL.Path)
+
+//line app/vmalert/web.qtpl:265
 	qw422016.N().S(`
     `)
 //line app/vmalert/web.qtpl:266
+	tpl.StreamHeader(qw422016, r, navItems, "")
+//line app/vmalert/web.qtpl:266
+	qw422016.N().S(`
+    `)
+//line app/vmalert/web.qtpl:268
 	var labelKeys []string
 	for k := range alert.Labels {
 		labelKeys = append(labelKeys, k)
@@ -891,28 +903,28 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) {
 	}
 	sort.Strings(annotationKeys)
 
-//line app/vmalert/web.qtpl:277
+//line app/vmalert/web.qtpl:279
 	qw422016.N().S(`
     <div class="display-6 pb-3 mb-3">`)
-//line app/vmalert/web.qtpl:278
+//line app/vmalert/web.qtpl:280
 	qw422016.E().S(alert.Name)
-//line app/vmalert/web.qtpl:278
+//line app/vmalert/web.qtpl:280
 	qw422016.N().S(`<span class="ms-2 badge `)
-//line app/vmalert/web.qtpl:278
+//line app/vmalert/web.qtpl:280
 	if alert.State == "firing" {
-//line app/vmalert/web.qtpl:278
+//line app/vmalert/web.qtpl:280
 		qw422016.N().S(`bg-danger`)
-//line app/vmalert/web.qtpl:278
+//line app/vmalert/web.qtpl:280
 	} else {
-//line app/vmalert/web.qtpl:278
+//line app/vmalert/web.qtpl:280
 		qw422016.N().S(` bg-warning text-dark`)
-//line app/vmalert/web.qtpl:278
+//line app/vmalert/web.qtpl:280
 	}
-//line app/vmalert/web.qtpl:278
+//line app/vmalert/web.qtpl:280
 	qw422016.N().S(`">`)
-//line app/vmalert/web.qtpl:278
+//line app/vmalert/web.qtpl:280
 	qw422016.E().S(alert.State)
-//line app/vmalert/web.qtpl:278
+//line app/vmalert/web.qtpl:280
 	qw422016.N().S(`</span></div>
     <div class="container border-bottom p-2">
       <div class="row">
@@ -921,9 +933,9 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) {
         </div>
         <div class="col">
           `)
-//line app/vmalert/web.qtpl:285
+//line app/vmalert/web.qtpl:287
 	qw422016.E().S(alert.ActiveAt.Format("2006-01-02T15:04:05Z07:00"))
-//line app/vmalert/web.qtpl:285
+//line app/vmalert/web.qtpl:287
 	qw422016.N().S(`
         </div>
       </div>
@@ -935,9 +947,9 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) {
         </div>
         <div class="col">
           <code><pre>`)
-//line app/vmalert/web.qtpl:295
+//line app/vmalert/web.qtpl:297
 	qw422016.E().S(alert.Expression)
-//line app/vmalert/web.qtpl:295
+//line app/vmalert/web.qtpl:297
 	qw422016.N().S(`</pre></code>
         </div>
       </div>
@@ -949,23 +961,23 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) {
         </div>
         <div class="col">
            `)
-//line app/vmalert/web.qtpl:305
+//line app/vmalert/web.qtpl:307
 	for _, k := range labelKeys {
-//line app/vmalert/web.qtpl:305
+//line app/vmalert/web.qtpl:307
 		qw422016.N().S(`
                 <span class="m-1 badge bg-primary">`)
-//line app/vmalert/web.qtpl:306
+//line app/vmalert/web.qtpl:308
 		qw422016.E().S(k)
-//line app/vmalert/web.qtpl:306
+//line app/vmalert/web.qtpl:308
 		qw422016.N().S(`=`)
-//line app/vmalert/web.qtpl:306
+//line app/vmalert/web.qtpl:308
 		qw422016.E().S(alert.Labels[k])
-//line app/vmalert/web.qtpl:306
+//line app/vmalert/web.qtpl:308
 		qw422016.N().S(`</span>
           `)
-//line app/vmalert/web.qtpl:307
+//line app/vmalert/web.qtpl:309
 	}
-//line app/vmalert/web.qtpl:307
+//line app/vmalert/web.qtpl:309
 	qw422016.N().S(`
         </div>
       </div>
@@ -977,24 +989,24 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) {
         </div>
         <div class="col">
            `)
-//line app/vmalert/web.qtpl:317
+//line app/vmalert/web.qtpl:319
 	for _, k := range annotationKeys {
-//line app/vmalert/web.qtpl:317
+//line app/vmalert/web.qtpl:319
 		qw422016.N().S(`
                 <b>`)
-//line app/vmalert/web.qtpl:318
+//line app/vmalert/web.qtpl:320
 		qw422016.E().S(k)
-//line app/vmalert/web.qtpl:318
+//line app/vmalert/web.qtpl:320
 		qw422016.N().S(`:</b><br>
                 <p>`)
-//line app/vmalert/web.qtpl:319
+//line app/vmalert/web.qtpl:321
 		qw422016.E().S(alert.Annotations[k])
-//line app/vmalert/web.qtpl:319
+//line app/vmalert/web.qtpl:321
 		qw422016.N().S(`</p>
           `)
-//line app/vmalert/web.qtpl:320
+//line app/vmalert/web.qtpl:322
 	}
-//line app/vmalert/web.qtpl:320
+//line app/vmalert/web.qtpl:322
 	qw422016.N().S(`
         </div>
       </div>
@@ -1005,14 +1017,18 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) {
           Group
         </div>
         <div class="col">
-           <a target="_blank" href="/groups#group-`)
-//line app/vmalert/web.qtpl:330
+           <a target="_blank" href="`)
+//line app/vmalert/web.qtpl:332
+	qw422016.E().S(prefix)
+//line app/vmalert/web.qtpl:332
+	qw422016.N().S(`groups#group-`)
+//line app/vmalert/web.qtpl:332
 	qw422016.E().S(alert.GroupID)
-//line app/vmalert/web.qtpl:330
+//line app/vmalert/web.qtpl:332
 	qw422016.N().S(`">`)
-//line app/vmalert/web.qtpl:330
+//line app/vmalert/web.qtpl:332
 	qw422016.E().S(alert.GroupID)
-//line app/vmalert/web.qtpl:330
+//line app/vmalert/web.qtpl:332
 	qw422016.N().S(`</a>
         </div>
       </div>
@@ -1024,132 +1040,132 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) {
         </div>
         <div class="col">
            <a target="_blank" href="`)
-//line app/vmalert/web.qtpl:340
+//line app/vmalert/web.qtpl:342
 	qw422016.E().S(alert.SourceLink)
-//line app/vmalert/web.qtpl:340
+//line app/vmalert/web.qtpl:342
 	qw422016.N().S(`">Link</a>
         </div>
       </div>
     </div>
     `)
-//line app/vmalert/web.qtpl:344
+//line app/vmalert/web.qtpl:346
 	tpl.StreamFooter(qw422016, r)
-//line app/vmalert/web.qtpl:344
+//line app/vmalert/web.qtpl:346
 	qw422016.N().S(`
 
 `)
-//line app/vmalert/web.qtpl:346
+//line app/vmalert/web.qtpl:348
 }
 
-//line app/vmalert/web.qtpl:346
+//line app/vmalert/web.qtpl:348
 func WriteAlert(qq422016 qtio422016.Writer, r *http.Request, alert *APIAlert) {
-//line app/vmalert/web.qtpl:346
+//line app/vmalert/web.qtpl:348
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line app/vmalert/web.qtpl:346
+//line app/vmalert/web.qtpl:348
 	StreamAlert(qw422016, r, alert)
-//line app/vmalert/web.qtpl:346
+//line app/vmalert/web.qtpl:348
 	qt422016.ReleaseWriter(qw422016)
-//line app/vmalert/web.qtpl:346
+//line app/vmalert/web.qtpl:348
 }
 
-//line app/vmalert/web.qtpl:346
+//line app/vmalert/web.qtpl:348
 func Alert(r *http.Request, alert *APIAlert) string {
-//line app/vmalert/web.qtpl:346
+//line app/vmalert/web.qtpl:348
 	qb422016 := qt422016.AcquireByteBuffer()
-//line app/vmalert/web.qtpl:346
+//line app/vmalert/web.qtpl:348
 	WriteAlert(qb422016, r, alert)
-//line app/vmalert/web.qtpl:346
+//line app/vmalert/web.qtpl:348
 	qs422016 := string(qb422016.B)
-//line app/vmalert/web.qtpl:346
+//line app/vmalert/web.qtpl:348
 	qt422016.ReleaseByteBuffer(qb422016)
-//line app/vmalert/web.qtpl:346
+//line app/vmalert/web.qtpl:348
 	return qs422016
-//line app/vmalert/web.qtpl:346
+//line app/vmalert/web.qtpl:348
 }
 
-//line app/vmalert/web.qtpl:348
+//line app/vmalert/web.qtpl:350
 func streambadgeState(qw422016 *qt422016.Writer, state string) {
-//line app/vmalert/web.qtpl:348
+//line app/vmalert/web.qtpl:350
 	qw422016.N().S(`
 `)
-//line app/vmalert/web.qtpl:350
+//line app/vmalert/web.qtpl:352
 	badgeClass := "bg-warning text-dark"
 	if state == "firing" {
 		badgeClass = "bg-danger"
 	}
 
-//line app/vmalert/web.qtpl:354
+//line app/vmalert/web.qtpl:356
 	qw422016.N().S(`
 <span class="badge `)
-//line app/vmalert/web.qtpl:355
+//line app/vmalert/web.qtpl:357
 	qw422016.E().S(badgeClass)
-//line app/vmalert/web.qtpl:355
+//line app/vmalert/web.qtpl:357
 	qw422016.N().S(`">`)
-//line app/vmalert/web.qtpl:355
+//line app/vmalert/web.qtpl:357
 	qw422016.E().S(state)
-//line app/vmalert/web.qtpl:355
+//line app/vmalert/web.qtpl:357
 	qw422016.N().S(`</span>
 `)
-//line app/vmalert/web.qtpl:356
+//line app/vmalert/web.qtpl:358
 }
 
-//line app/vmalert/web.qtpl:356
+//line app/vmalert/web.qtpl:358
 func writebadgeState(qq422016 qtio422016.Writer, state string) {
-//line app/vmalert/web.qtpl:356
+//line app/vmalert/web.qtpl:358
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line app/vmalert/web.qtpl:356
+//line app/vmalert/web.qtpl:358
 	streambadgeState(qw422016, state)
-//line app/vmalert/web.qtpl:356
+//line app/vmalert/web.qtpl:358
 	qt422016.ReleaseWriter(qw422016)
-//line app/vmalert/web.qtpl:356
+//line app/vmalert/web.qtpl:358
 }
 
-//line app/vmalert/web.qtpl:356
+//line app/vmalert/web.qtpl:358
 func badgeState(state string) string {
-//line app/vmalert/web.qtpl:356
+//line app/vmalert/web.qtpl:358
 	qb422016 := qt422016.AcquireByteBuffer()
-//line app/vmalert/web.qtpl:356
+//line app/vmalert/web.qtpl:358
 	writebadgeState(qb422016, state)
-//line app/vmalert/web.qtpl:356
+//line app/vmalert/web.qtpl:358
 	qs422016 := string(qb422016.B)
-//line app/vmalert/web.qtpl:356
+//line app/vmalert/web.qtpl:358
 	qt422016.ReleaseByteBuffer(qb422016)
-//line app/vmalert/web.qtpl:356
+//line app/vmalert/web.qtpl:358
 	return qs422016
-//line app/vmalert/web.qtpl:356
+//line app/vmalert/web.qtpl:358
 }
 
-//line app/vmalert/web.qtpl:358
+//line app/vmalert/web.qtpl:360
 func streambadgeRestored(qw422016 *qt422016.Writer) {
-//line app/vmalert/web.qtpl:358
+//line app/vmalert/web.qtpl:360
 	qw422016.N().S(`
 <span class="badge bg-warning text-dark" title="Alert state was restored after the service restart from remote storage">restored</span>
 `)
-//line app/vmalert/web.qtpl:360
+//line app/vmalert/web.qtpl:362
 }
 
-//line app/vmalert/web.qtpl:360
+//line app/vmalert/web.qtpl:362
 func writebadgeRestored(qq422016 qtio422016.Writer) {
-//line app/vmalert/web.qtpl:360
+//line app/vmalert/web.qtpl:362
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line app/vmalert/web.qtpl:360
+//line app/vmalert/web.qtpl:362
 	streambadgeRestored(qw422016)
-//line app/vmalert/web.qtpl:360
+//line app/vmalert/web.qtpl:362
 	qt422016.ReleaseWriter(qw422016)
-//line app/vmalert/web.qtpl:360
+//line app/vmalert/web.qtpl:362
 }
 
-//line app/vmalert/web.qtpl:360
+//line app/vmalert/web.qtpl:362
 func badgeRestored() string {
-//line app/vmalert/web.qtpl:360
+//line app/vmalert/web.qtpl:362
 	qb422016 := qt422016.AcquireByteBuffer()
-//line app/vmalert/web.qtpl:360
+//line app/vmalert/web.qtpl:362
 	writebadgeRestored(qb422016)
-//line app/vmalert/web.qtpl:360
+//line app/vmalert/web.qtpl:362
 	qs422016 := string(qb422016.B)
-//line app/vmalert/web.qtpl:360
+//line app/vmalert/web.qtpl:362
 	qt422016.ReleaseByteBuffer(qb422016)
-//line app/vmalert/web.qtpl:360
+//line app/vmalert/web.qtpl:362
 	return qs422016
-//line app/vmalert/web.qtpl:360
+//line app/vmalert/web.qtpl:362
 }
diff --git a/app/vmalert/web_test.go b/app/vmalert/web_test.go
index ef639b11bc..af77967ec4 100644
--- a/app/vmalert/web_test.go
+++ b/app/vmalert/web_test.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"encoding/json"
+	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"reflect"
@@ -29,7 +30,7 @@ func TestHandler(t *testing.T) {
 		t.Helper()
 		resp, err := http.Get(url)
 		if err != nil {
-			t.Errorf("unexpected err %s", err)
+			t.Fatalf("unexpected err %s", err)
 		}
 		if code != resp.StatusCode {
 			t.Errorf("unexpected status code %d want %d", resp.StatusCode, code)
@@ -47,20 +48,72 @@ func TestHandler(t *testing.T) {
 	}
 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) }))
 	defer ts.Close()
+
+	t.Run("/", func(t *testing.T) {
+		getResp(ts.URL, nil, 200)
+		getResp(ts.URL+"/vmalert", nil, 200)
+		getResp(ts.URL+"/vmalert/home", nil, 200)
+	})
+
 	t.Run("/api/v1/alerts", func(t *testing.T) {
 		lr := listAlertsResponse{}
 		getResp(ts.URL+"/api/v1/alerts", &lr, 200)
 		if length := len(lr.Data.Alerts); length != 1 {
 			t.Errorf("expected 1 alert got %d", length)
 		}
+
+		lr = listAlertsResponse{}
+		getResp(ts.URL+"/vmalert/api/v1/alerts", &lr, 200)
+		if length := len(lr.Data.Alerts); length != 1 {
+			t.Errorf("expected 1 alert got %d", length)
+		}
 	})
+	t.Run("/api/v1/alert?alertID&groupID", func(t *testing.T) {
+		expAlert := ar.newAlertAPI(*ar.alerts[0])
+		alert := &APIAlert{}
+		getResp(ts.URL+"/"+expAlert.APILink(), alert, 200)
+		if !reflect.DeepEqual(alert, expAlert) {
+			t.Errorf("expected %v is equal to %v", alert, expAlert)
+		}
+
+		alert = &APIAlert{}
+		getResp(ts.URL+"/vmalert/"+expAlert.APILink(), alert, 200)
+		if !reflect.DeepEqual(alert, expAlert) {
+			t.Errorf("expected %v is equal to %v", alert, expAlert)
+		}
+	})
+
+	t.Run("/api/v1/alert?badParams", func(t *testing.T) {
+		params := fmt.Sprintf("?%s=0&%s=1", paramGroupID, paramAlertID)
+		getResp(ts.URL+"/api/v1/alert"+params, nil, 404)
+		getResp(ts.URL+"/vmalert/api/v1/alert"+params, nil, 404)
+
+		params = fmt.Sprintf("?%s=1&%s=0", paramGroupID, paramAlertID)
+		getResp(ts.URL+"/api/v1/alert"+params, nil, 404)
+		getResp(ts.URL+"/vmalert/api/v1/alert"+params, nil, 404)
+
+		// bad request, alertID is missing
+		params = fmt.Sprintf("?%s=1", paramGroupID)
+		getResp(ts.URL+"/api/v1/alert"+params, nil, 400)
+		getResp(ts.URL+"/vmalert/api/v1/alert"+params, nil, 400)
+	})
+
 	t.Run("/api/v1/rules", func(t *testing.T) {
 		lr := listGroupsResponse{}
 		getResp(ts.URL+"/api/v1/rules", &lr, 200)
 		if length := len(lr.Data.Groups); length != 1 {
 			t.Errorf("expected 1 group got %d", length)
 		}
+
+		lr = listGroupsResponse{}
+		getResp(ts.URL+"/vmalert/api/v1/rules", &lr, 200)
+		if length := len(lr.Data.Groups); length != 1 {
+			t.Errorf("expected 1 group got %d", length)
+		}
 	})
+
+	// check deprecated links support
+	// TODO: remove as soon as deprecated links removed
 	t.Run("/api/v1/0/0/status", func(t *testing.T) {
 		alert := &APIAlert{}
 		getResp(ts.URL+"/api/v1/0/0/status", alert, 200)
@@ -75,7 +128,5 @@ func TestHandler(t *testing.T) {
 	t.Run("/api/v1/1/0/status", func(t *testing.T) {
 		getResp(ts.URL+"/api/v1/1/0/status", nil, 404)
 	})
-	t.Run("/", func(t *testing.T) {
-		getResp(ts.URL, nil, 200)
-	})
+
 }
diff --git a/app/vmalert/web_types.go b/app/vmalert/web_types.go
index fad7be4710..b2e150807e 100644
--- a/app/vmalert/web_types.go
+++ b/app/vmalert/web_types.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"fmt"
 	"time"
 )
 
@@ -33,6 +34,18 @@ type APIAlert struct {
 	Restored bool `json:"restored"`
 }
 
+// WebLink returns a link to the alert which can be used in UI.
+func (aa *APIAlert) WebLink() string {
+	return fmt.Sprintf("alert?%s=%s&%s=%s",
+		paramGroupID, aa.GroupID, paramAlertID, aa.ID)
+}
+
+// APILink returns a link to the alert's JSON representation.
+func (aa *APIAlert) APILink() string {
+	return fmt.Sprintf("api/v1/alert?%s=%s&%s=%s",
+		paramGroupID, aa.GroupID, paramAlertID, aa.ID)
+}
+
 // APIGroup represents Group for WEB view
 // https://github.com/prometheus/compliance/blob/main/alert_generator/specification.md#get-apiv1rules
 type APIGroup struct {
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 095cc9f7d2..98b18d3ef3 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -18,6 +18,12 @@ The following tip changes can be tested by building VictoriaMetrics components f
 **Update notes:** this release introduces backwards-incompatible changes to `vm_partial_results_total` metric by changing its labels to be consistent with `vm_requests_total` metric.
 If you use alerting rules or Grafana dashboards, which rely on this metric, then they must be updated. The official dashboards for VictoriaMetrics don't use this metric.
 
+[vmalert](https://docs.victoriametrics.com/vmalert.html) routing was updated according to [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2825).
+The change may affect users with `-http.pathPrefix` flag configured for vmalert. With the update, configuring this flag is no longer needed. Here's [why](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2799#issuecomment-1171392005).
+
+* CHANGE: [vmalert](https://docs.victoriametrics.com/vmalert.html): deprecate alert's status link `/api/v1/<groupID>/<alertID>/status` in favour of `api/v1/alert?group_id=<group_id>&alert_id=<alert_id>"`.
+The old alert's status link will be still supported for a few versions but will be removed in the future.
+
 * FEATURE: [cluster version of VictoriaMetrics](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html): add support for querying lower-level `vmselect` nodes from upper-level `vmselect` nodes. This makes possible to build multi-level cluster setups for global querying view and HA purposes without the need to use [Promxy](https://github.com/jacksontj/promxy). See [these docs](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#multi-level-cluster-setup) and [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2778).
 * FEATURE: add `-search.setLookbackToStep` command-line flag, which enables InfluxDB-like gap filling during querying. See [these docs](https://docs.victoriametrics.com/guides/migrate-from-influx.html) for details.
 * FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add an UI for [query tracing](https://docs.victoriametrics.com/#query-tracing). It can be enabled by clicking `trace query` checkbox and re-running the query. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2703).
diff --git a/docs/vmalert.md b/docs/vmalert.md
index e11cb61b76..0da33a649c 100644
--- a/docs/vmalert.md
+++ b/docs/vmalert.md
@@ -485,7 +485,7 @@ or time series modification via [relabeling](https://docs.victoriametrics.com/vm
 * `http://<vmalert-addr>` - UI;
 * `http://<vmalert-addr>/api/v1/rules` - list of all loaded groups and rules;
 * `http://<vmalert-addr>/api/v1/alerts` - list of all active alerts;
-* `http://<vmalert-addr>/api/v1/<groupID>/<alertID>/status"` - get alert status by ID.
+* `http://<vmalert-addr>/vmalert/api/v1/alert?group_id=<group_id>&alert_id=<alert_id>"` - get alert status by ID.
   Used as alert source in AlertManager.
 * `http://<vmalert-addr>/metrics` - application metrics.
 * `http://<vmalert-addr>/-/reload` - hot configuration reload.
@@ -685,7 +685,7 @@ The shortlist of configuration flags is the following:
      How often to evaluate the rules (default 1m0s)
   -external.alert.source string
      External Alert Source allows to override the Source link for alerts sent to AlertManager for cases where you want to build a custom link to Grafana, Prometheus or any other service.
-     eg. 'explore?orgId=1&left=[\"now-1h\",\"now\",\"VictoriaMetrics\",{\"expr\": \"{{$expr|quotesEscape|crlfEscape|queryEscape}}\"},{\"mode\":\"Metrics\"},{\"ui\":[true,true,true,\"none\"]}]'.If empty '/api/v1/:groupID/alertID/status' is used
+     eg. 'explore?orgId=1&left=[\"now-1h\",\"now\",\"VictoriaMetrics\",{\"expr\": \"{{$expr|quotesEscape|crlfEscape|queryEscape}}\"},{\"mode\":\"Metrics\"},{\"ui\":[true,true,true,\"none\"]}]'.If empty '/vmalert/api/v1/alert?group_id=&alert_id=' is used
   -external.label array
      Optional label in the form 'Name=value' to add to all generated recording rules and alerts. Pass multiple -label flags in order to add multiple label sets.
      Supports an array of values separated by comma or specified via multiple flags.