console: app settings tab — identity, domains and TLS (Heroku's answer to where domains live)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 files changed,
+60
−0
internal/console/server.go+11 −0internal/console/server_test.go+17 −0internal/console/templates/app_settings.html+32 −0
diff --git a/internal/console/server.go b/internal/console/server.go| index 7e6e75d86..749bc036c 100644 |
| --- a/internal/console/server.go |
| +++ b/internal/console/server.go |
| @@ -263,6 +263,7 @@ func NewServer(cfg Config, st *Store) (*Server, error) { |
| 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("GET /a/{sqid}/apps/{app}/settings", s.handleAppSettings) |
| 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) |
| @@ -1395,6 +1396,16 @@ func (s *Server) handleAppActivity(w http.ResponseWriter, r *http.Request) { |
| s.render(w, http.StatusOK, "app_activity", p) |
| } |
| +// handleAppSettings is the app's own record: identity, and its domains — |
| +// the two things about an app that aren't a deploy. |
| +func (s *Server) handleAppSettings(w http.ResponseWriter, r *http.Request) { |
| + p, _, ok := s.appPage(w, r, "settings") |
| + if !ok { |
| + return |
| + } |
| + s.render(w, http.StatusOK, "app_settings", p) |
| +} |
| + |
| // eventsCap bounds the app page's recent-activity list. |
| const eventsCap = 10 |
diff --git a/internal/console/server_test.go b/internal/console/server_test.go| index 6c76a994c..708f8f30e 100644 |
| --- a/internal/console/server_test.go |
| +++ b/internal/console/server_test.go |
| @@ -1078,6 +1078,23 @@ func TestActivityTab(t *testing.T) { |
| assertNonMember404(t, fx, "/apps/kass/activity") |
| } |
| +func TestAppSettingsTab(t *testing.T) { |
| + fx := newConsoleFixture(t) |
| + c := signInPaul(t, fx) |
| + status, _, body := get(t, c, fx.u("/apps/kass/settings")) |
| + if status != 200 { |
| + t.Fatalf("GET app settings = %d", status) |
| + } |
| + for _, want := range []string{ |
| + `aria-current="page">Settings</a>`, "<h2>Identity</h2>", "Domains & TLS", |
| + } { |
| + if !strings.Contains(body, want) { |
| + t.Fatalf("app settings missing %q", want) |
| + } |
| + } |
| + assertNonMember404(t, fx, "/apps/kass/settings") |
| +} |
| + |
| func TestPromotionsDisabledWithoutKey(t *testing.T) { |
| fx := newConsoleFixtureKeyed(t, false) |
| c := signInPaul(t, fx) |
diff --git a/internal/console/templates/app_settings.html b/internal/console/templates/app_settings.html| new file mode 100644 |
| index 000000000..02017bcf5 |
| --- /dev/null |
| +++ b/internal/console/templates/app_settings.html |
| @@ -0,0 +1,32 @@ |
| +{{define "app_settings"}}{{template "top" .}} |
| +{{if .Flash}}<p class="banner">{{.Flash}}</p>{{end}} |
| +{{if .FlashErr}}<p class="banner error">{{.FlashErr}}</p>{{end}} |
| +{{template "appnav" .}} |
| + |
| +<h2>Identity</h2> |
| +<div class="tablewrap"> |
| +<table> |
| + <tr><th>App</th><th>Account</th></tr> |
| + <tr><td><code>{{.Detail.App}}</code></td><td>{{.Account.Name}} <code>{{.Account.Sqid}}</code></td></tr> |
| +</table> |
| +</div> |
| +<h2>Domains & TLS</h2> |
| +{{if .Detail.Domains}} |
| +<div class="tablewrap"> |
| +<table> |
| + <tr><th>Domain</th><th>Type</th><th>DNS</th><th>TLS</th></tr> |
| + {{range .Detail.Domains}} |
| + <tr> |
| + <td><code>{{.Domain}}</code></td> |
| + <td>{{.Type}}</td> |
| + <td>{{if .DNS}}{{.DNS}}{{else}}<span title="unknown — not checked yet">—</span>{{end}}</td> |
| + <td>{{if .TLS}}{{.TLS}}{{else}}<span title="unknown">—</span>{{end}}</td> |
| + </tr> |
| + {{if .Guidance}} |
| + <tr><td colspan="4" class="small">{{.Guidance}}</td></tr> |
| + {{end}} |
| + {{end}} |
| +</table> |
| +</div> |
| +{{else}}<p class="small">No domains yet — the first shipped route brings one.</p>{{end}} |
| +{{template "bottom" .}}{{end}} |