carlos / platform Public

Connect console instance status, activity and live logs

Paul Campbell pushed by paul@keymail.dev 2d77e0af77d6b56223f119af6574ad7367ead103 parent 64972fd
10 files changed, +79 −16
  • internal/console/activity_test.go +25 −0
  • internal/console/browserrestart.go +5 −1
  • internal/console/deployments.go +2 −2
  • internal/console/deployments_test.go +4 −1
  • internal/console/instancepage.go +6 −2
  • internal/console/instancepage_test.go +28 −0
  • internal/console/overview.go +1 −2
  • internal/console/server.go +2 −7
  • internal/console/templates/app_deployments.html +1 −0
  • internal/console/templates/app_logs.html +5 −1

CI passed — run details

diff --git a/internal/console/activity_test.go b/internal/console/activity_test.go
index 721f46a5b..181dca877 100644
--- a/internal/console/activity_test.go
+++ b/internal/console/activity_test.go
@@ -5,6 +5,8 @@ import (
"strings"
"testing"
"time"
+
+ "github.com/carlosframework/platform/internal/registry"
)
func TestValidActivityMessage(t *testing.T) {
@@ -23,6 +25,29 @@ func TestValidActivityMessage(t *testing.T) {
}
}
+func TestActivityDoesNotLinkPlatformAliasAsInstance(t *testing.T) {
+ fx := newConsoleFixture(t)
+ c := signInPaul(t, fx)
+ host := "platform-alias.example.com"
+ fx.addRoute(t, registry.Route{Host: host, App: "kass", Account: fx.sqid, Kind: "static", Platform: true})
+ if err := fx.st.RecordActivity(OpsAccountID, "kass", "paul@keymail.dev", Activity{
+ Action: "instance-create", TargetType: "instance", Target: host, Detail: "created instance " + host,
+ }, time.Now()); err != nil {
+ t.Fatal(err)
+ }
+
+ status, _, body := get(t, c, fx.u("/apps/kass/activity"))
+ if status != 200 {
+ t.Fatalf("status = %d", status)
+ }
+ if !strings.Contains(body, host) {
+ t.Fatalf("activity target missing: %s", body)
+ }
+ if strings.Contains(body, `/instances/`+host) {
+ t.Fatalf("platform alias target linked as an instance: %s", body)
+ }
+}
+
func TestCheckedActivityMessageRejectsTrailingControl(t *testing.T) {
for _, message := range []string{"description\n", "\tdescription", "description\r"} {
w := httptest.NewRecorder()
diff --git a/internal/console/browserrestart.go b/internal/console/browserrestart.go
index 1429f13f6..287b89fc6 100644
--- a/internal/console/browserrestart.go
+++ b/internal/console/browserrestart.go
@@ -136,7 +136,11 @@ func (s *Server) handleBrowserRestart(w http.ResponseWriter, r *http.Request, ho
}
detail := strings.ReplaceAll(words.Audit, "{target}", target)
success := strings.ReplaceAll(words.Success, "{target}", target)
- s.audit(acct, app, addr, "restart", detail)
+ targetType := "app"
+ if host != "" {
+ targetType = "instance"
+ }
+ s.auditActivity(acct, app, addr, Activity{Action: "restart", TargetType: targetType, Target: target, Detail: detail})
s.bumpSerial(ctx)
s.setFlash(w, "ok", success)
}
diff --git a/internal/console/deployments.go b/internal/console/deployments.go
index 2acb62939..973747de8 100644
--- a/internal/console/deployments.go
+++ b/internal/console/deployments.go
@@ -39,7 +39,7 @@ func assembleDeploymentTargets(view *AppView, account string) []deploymentTarget
}
versions := map[string]map[string]int{}
for _, rt := range view.Routes {
- if rt.Account != account {
+ if rt.Account != account || rt.Platform {
continue
}
ch := rt.Channel
@@ -51,7 +51,7 @@ func assembleDeploymentTargets(view *AppView, account string) []deploymentTarget
}
group := groups[ch]
group.Total++
- if rt.Version == "" {
+ if (rt.Kind != "" && rt.Kind != "instance") || rt.Version == "" {
group.Unknown++
continue
}
diff --git a/internal/console/deployments_test.go b/internal/console/deployments_test.go
index 42ac77249..5f816b21f 100644
--- a/internal/console/deployments_test.go
+++ b/internal/console/deployments_test.go
@@ -12,6 +12,9 @@ func TestDeploymentTargetsKeepObservationSeparate(t *testing.T) {
view := &AppView{
Channels: []ChannelView{{Name: "edge", Version: "new"}, {Name: "stable", Err: "unavailable"}, {Name: "empty"}},
Routes: []RouteView{
+ {Account: "ours", Channel: "edge", Version: "alias", Platform: true},
+ {Account: "ours", Channel: "edge", Version: "new", Kind: "static"},
+ {Account: "ours", Channel: "edge", Version: "old", Kind: "service"},
{Account: "ours", Channel: "edge", Version: "old"},
{Account: "ours", Channel: "edge", Version: "new"},
{Account: "ours", Channel: "edge", Version: "old"},
@@ -23,7 +26,7 @@ func TestDeploymentTargetsKeepObservationSeparate(t *testing.T) {
}
want := []deploymentTarget{
{Channel: "canary/custom", Total: 1, Versions: []deploymentObservedVersion{{Version: "custom", Count: 1}}},
- {Channel: "edge", Target: "new", Total: 4, Unknown: 1, Versions: []deploymentObservedVersion{{Version: "new", Count: 1}, {Version: "old", Count: 2}}},
+ {Channel: "edge", Target: "new", Total: 6, Unknown: 3, Versions: []deploymentObservedVersion{{Version: "new", Count: 1}, {Version: "old", Count: 2}}},
{Channel: "empty"},
{Channel: "stable", TargetErr: "unavailable", Total: 1, Versions: []deploymentObservedVersion{{Version: "running", Count: 1}}},
}
diff --git a/internal/console/instancepage.go b/internal/console/instancepage.go
index e454687eb..23400b7f8 100644
--- a/internal/console/instancepage.go
+++ b/internal/console/instancepage.go
@@ -28,7 +28,7 @@ type instanceGroup struct {
func ownedInstanceRoutes(routes []RouteView, account string) []RouteView {
var out []RouteView
for _, rt := range routes {
- if rt.Account == account {
+ if rt.Account == account && !rt.Platform {
out = append(out, rt)
}
}
@@ -122,7 +122,7 @@ func (s *Server) handleAppInstance(w http.ResponseWriter, r *http.Request) {
}
var selected *RouteView
for _, rt := range p.Detail.Routes {
- if rt.Host == host && rt.Account == acct.Sqid {
+ if rt.Host == host && rt.Account == acct.Sqid && !rt.Platform {
copy := rt
selected = &copy
break
@@ -136,6 +136,10 @@ func (s *Server) handleAppInstance(w http.ResponseWriter, r *http.Request) {
view.Routes = []RouteView{*selected}
metrics := s.appResourceMetrics(r.Context(), &view, acct)
rows := assembleInstanceRows(&view, acct, metrics)
+ if len(rows) == 0 {
+ http.NotFound(w, r)
+ return
+ }
detail := &instanceDetail{Row: rows[0]}
for _, ch := range p.Detail.Channels {
if ch.Name == selected.Channel {
diff --git a/internal/console/instancepage_test.go b/internal/console/instancepage_test.go
index 62b50e354..a3dee4db4 100644
--- a/internal/console/instancepage_test.go
+++ b/internal/console/instancepage_test.go
@@ -45,6 +45,23 @@ func TestInstanceGroupingPaginatesWithoutLoss(t *testing.T) {
}
}
+func TestInstanceRowsExcludePlatformAliases(t *testing.T) {
+ routes := []RouteView{
+ {Host: "app.example", Account: "ours", Kind: "instance"},
+ {Host: "static.example", Account: "ours", Kind: "static"},
+ {Host: "alias.example", Account: "ours", Kind: "static", Platform: true},
+ {Host: "foreign.example", Account: "theirs", Kind: "instance"},
+ }
+ owned := ownedInstanceRoutes(routes, "ours")
+ if len(owned) != 2 || owned[0].Host != "app.example" || owned[1].Host != "static.example" {
+ t.Fatalf("owned instance routes = %+v", owned)
+ }
+ rows := assembleInstanceRows(&AppView{Routes: routes}, &Account{Sqid: "ours"}, nil)
+ if len(rows) != 2 || rows[0].Host != "app.example" || rows[1].Host != "static.example" {
+ t.Fatalf("instance rows = %+v", rows)
+ }
+}
+
func TestInstanceChannelFilterAndLinks(t *testing.T) {
rows := []InstanceRow{{Host: "a.example.com", Channel: "canary/a+b", HealthWord: "Running"}, {Host: "b.example.com", Channel: "edge", HealthWord: "Asleep"}}
q := instanceQuery{Channel: "canary/a+b", Group: true}
@@ -137,6 +154,17 @@ func TestInstanceDetailOwnsRouteAndAliases(t *testing.T) {
}
}
+func TestInstanceDetailRejectsPlatformAlias(t *testing.T) {
+ fx := newConsoleFixture(t)
+ c := signInPaul(t, fx)
+ fx.addRoute(t, registry.Route{Host: "platform-alias.example.com", App: "kass", Account: fx.sqid, Kind: "static", Platform: true})
+
+ status, _, body := get(t, c, fx.u("/apps/kass/instances/platform-alias.example.com"))
+ if status != http.StatusNotFound || body != "404 page not found\n" {
+ t.Fatalf("platform alias detail = %d %q", status, body)
+ }
+}
+
func TestInstanceListLinksToDetailAndKeepsHealthShort(t *testing.T) {
fx := newConsoleFixture(t)
c := signInPaul(t, fx)
diff --git a/internal/console/overview.go b/internal/console/overview.go
index d5ba9e4f3..108537bfa 100644
--- a/internal/console/overview.go
+++ b/internal/console/overview.go
@@ -201,7 +201,6 @@ type InstanceRow struct {
// plain word — the column's whole job.
HealthWord string
Restartable bool
- RestartURL string
}
// assembleInstanceRows joins the app's routes with the sweep's readings, in
@@ -215,7 +214,7 @@ func assembleInstanceRows(view *AppView, acct *Account, metrics []InstanceMetric
}
rows := make([]InstanceRow, 0, len(view.Routes))
for _, rt := range view.Routes {
- if rt.Account != acct.Sqid {
+ if rt.Account != acct.Sqid || rt.Platform {
continue
}
status := rt.Status
diff --git a/internal/console/server.go b/internal/console/server.go
index 1b91ff9ed..e7b54de51 100644
--- a/internal/console/server.go
+++ b/internal/console/server.go
@@ -2883,11 +2883,6 @@ func (s *Server) handleAppInstances(w http.ResponseWriter, r *http.Request) {
}
pg := pageInstanceRows(rows, q, instancePageSize)
p.InstanceRows = pg.Rows
- for i := range p.InstanceRows {
- if p.InstanceRows[i].Restartable {
- p.InstanceRows[i].RestartURL = p.Base + "/apps/" + url.PathEscape(p.AppName) + "/instances/" + url.PathEscape(p.InstanceRows[i].Host) + "/restart"
- }
- }
p.InstancePage = pg
p.InstanceQuery = q
p.InstanceChannels = instanceChannels(rows)
@@ -3139,7 +3134,7 @@ func (s *Server) handleAppActivity(w http.ResponseWriter, r *http.Request) {
// plain text; an activity link must never cross the membership boundary.
instances := make(map[string]bool)
for _, route := range p.Detail.Routes {
- if route.Account == acct.Sqid {
+ if route.Account == acct.Sqid && !route.Platform {
instances[route.Host] = true
}
}
@@ -3151,7 +3146,7 @@ func (s *Server) handleAppActivity(w http.ResponseWriter, r *http.Request) {
events[i].TargetURL = p.Nav + "/apps/" + url.PathEscape(p.AppName) + "/deployments"
}
if events[i].TargetType == "instance" && instances[events[i].Target] {
- events[i].TargetURL = p.Base + "/apps/" + url.PathEscape(p.AppName) +
+ events[i].TargetURL = p.Nav + "/apps/" + url.PathEscape(p.AppName) +
"/instances/" + url.PathEscape(events[i].Target)
}
}
diff --git a/internal/console/templates/app_deployments.html b/internal/console/templates/app_deployments.html
index d86613886..61cfebdfd 100644
--- a/internal/console/templates/app_deployments.html
+++ b/internal/console/templates/app_deployments.html
@@ -2,6 +2,7 @@
{{if .Flash}}<p class="banner">{{.Flash}}</p>{{end}}
{{if .FlashErr}}<p class="banner error">{{.FlashErr}}</p>{{end}}
{{template "appnav" .}}
+{{template "accountreviewbanner" .}}
<h2>{{.DeploymentsCopy.Title}}</h2>
<nav class="tabs" aria-label="{{.DeploymentsCopy.Title}}">
<a href="{{.Nav}}/apps/{{.AppName}}/deployments" aria-current="page">{{.DeploymentsCopy.Title}}</a>
diff --git a/internal/console/templates/app_logs.html b/internal/console/templates/app_logs.html
index 903a27789..fdfe6ed66 100644
--- a/internal/console/templates/app_logs.html
+++ b/internal/console/templates/app_logs.html
@@ -135,7 +135,11 @@ site served from files alone has no app output.</p>
var q = new URLSearchParams(activeQuery);
var hadCursor = Boolean(cursor);
q.set("limit", "200");
- if (cursor) q.set("cursor", cursor);
+ if (cursor) {
+ q.set("cursor", cursor);
+ q.delete("since");
+ q.delete("until");
+ }
request = new AbortController();
timedOut = false;
var thisRequest = request;