carlos / platform Public

console: app settings tab — identity, domains and TLS (Heroku's answer to where domains live)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell d5a8a37e8c69d4f8ed75b64c590fb636f96f7731 parent 2d22733
3 files changed, +60 −0
  • internal/console/server.go +11 −0
  • internal/console/server_test.go +17 −0
  • internal/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 &amp; 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 &amp; 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}}