carlos / platform Public

console: activity tab — the audit trail gets a page and a 50-row cap

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 2d22733d708fb6d4af13e22a90095e795a2ba361 parent e1e9101
3 files changed, +50 −0
  • internal/console/server.go +18 −0
  • internal/console/server_test.go +15 −0
  • internal/console/templates/app_activity.html +17 −0
diff --git a/internal/console/server.go b/internal/console/server.go
index a20de8739..7e6e75d86 100644
--- a/internal/console/server.go
+++ b/internal/console/server.go
@@ -262,6 +262,7 @@ func NewServer(cfg Config, st *Store) (*Server, error) {
s.mux.HandleFunc("GET /a/{sqid}/apps/{app}", s.handleApp)
s.mux.HandleFunc("GET /a/{sqid}/apps/{app}/releases", s.handleAppReleases)
s.mux.HandleFunc("GET /a/{sqid}/apps/{app}/instances", s.handleAppInstances)
+ s.mux.HandleFunc("GET /a/{sqid}/apps/{app}/activity", s.handleAppActivity)
s.mux.HandleFunc("POST /a/{sqid}/apps/{app}/promote", s.handlePromote)
s.mux.HandleFunc("POST /a/{sqid}/apps/{app}/rollback", s.handleRollback)
s.mux.HandleFunc("POST /a/{sqid}/apps/{app}/production", s.handleProduction)
@@ -1380,9 +1381,26 @@ func (s *Server) handleAppInstances(w http.ResponseWriter, r *http.Request) {
s.render(w, http.StatusOK, "app_instances", p)
}
+// handleAppActivity is the console's own audit trail for this app.
+func (s *Server) handleAppActivity(w http.ResponseWriter, r *http.Request) {
+ p, acct, ok := s.appPage(w, r, "activity")
+ if !ok {
+ return
+ }
+ if events, err := s.store.Events(acct.ID, p.AppName, activityCap); err != nil {
+ log.Printf("console: events %s: %v", p.AppName, err)
+ } else {
+ p.Events = events
+ }
+ s.render(w, http.StatusOK, "app_activity", p)
+}
+
// eventsCap bounds the app page's recent-activity list.
const eventsCap = 10
+// activityCap bounds the activity tab's own listing.
+const activityCap = 50 // the activity tab has the room the overview strip never did
+
// audit best-effort-records an owner action: a failed audit line is logged,
// never surfaced, and never fails the action it records.
func (s *Server) audit(acct *Account, app, addr, action, detail string) {
diff --git a/internal/console/server_test.go b/internal/console/server_test.go
index 3a05a1476..6c76a994c 100644
--- a/internal/console/server_test.go
+++ b/internal/console/server_test.go
@@ -1063,6 +1063,21 @@ func TestRecentActivity(t *testing.T) {
}
}
+func TestActivityTab(t *testing.T) {
+ fx := newConsoleFixture(t)
+ c := signInPaul(t, fx)
+ status, _, body := get(t, c, fx.u("/apps/kass/activity"))
+ if status != 200 {
+ t.Fatalf("GET activity tab = %d", status)
+ }
+ for _, want := range []string{`aria-current="page">Activity</a>`, "CLI promotions don"} {
+ if !strings.Contains(body, want) {
+ t.Fatalf("activity tab missing %q", want)
+ }
+ }
+ assertNonMember404(t, fx, "/apps/kass/activity")
+}
+
func TestPromotionsDisabledWithoutKey(t *testing.T) {
fx := newConsoleFixtureKeyed(t, false)
c := signInPaul(t, fx)
diff --git a/internal/console/templates/app_activity.html b/internal/console/templates/app_activity.html
new file mode 100644
index 000000000..814d9e917
--- /dev/null
+++ b/internal/console/templates/app_activity.html
@@ -0,0 +1,17 @@
+{{define "app_activity"}}{{template "top" .}}
+{{if .Flash}}<p class="banner">{{.Flash}}</p>{{end}}
+{{if .FlashErr}}<p class="banner error">{{.FlashErr}}</p>{{end}}
+{{template "appnav" .}}
+
+{{if .Events}}
+<div class="tablewrap">
+<table>
+ <tr><th>When</th><th>Who</th><th>What</th></tr>
+ {{range .Events}}
+ <tr><td>{{fmtTime .CreatedAt}}</td><td>{{.Address}}</td><td>{{.Detail}}</td></tr>
+ {{end}}
+</table>
+</div>
+{{else}}<p class="small">Nothing yet — promotes, rollbacks and canaries made in this console will show here.</p>{{end}}
+<p class="small">Actions taken through this console only — CLI promotions don't appear here.</p>
+{{template "bottom" .}}{{end}}