1 Commits
Author SHA1 Message Date
sneak f68e755bd2 Show the deploy branch in the app page title and early in the app list (closes #240)
Check / check (pull_request) Skipped
The app page shows the configured branch as a neutral label next to the
status badge; the line under the title now shows only the repository.
The app list moves the Branch column to right after Name and lets the
table scroll sideways inside its card, so narrow screens no longer cut
columns off. A new test renders both pages for an app on a non-main
branch.

Model: opus-5-5
2026-09-29 09:47:13 +00:00
5 changed files with 54 additions and 91 deletions
+3 -4
View File
@@ -21,10 +21,9 @@ regress.
# Completed Steps
- 2026-09-29: The app page shows the app's branch as a label in its title, next
to the status badge, instead of after the repository under it (#240).
- 2026-09-29: An app's deployments page now lists only its 10 most recent
deployments, newest first, instead of 50 (#238).
to the status badge, instead of after the repository under it; the app list
shows the Branch column right after Name and scrolls sideways on narrow
screens instead of cutting columns off (#240).
- 2026-09-29: `docker-compose.yml` now sets `UPAAS_PORT` to 8080 as well as
`PORT`, since upaas reads `UPAAS_PORT` first and a `UPAAS_PORT` in `.env` made
+1 -1
View File
@@ -28,7 +28,7 @@ const (
// recentDeploymentsLimit is the number of recent deployments to show.
recentDeploymentsLimit = 5
// deploymentsHistoryLimit is the number of deployments to show in history.
deploymentsHistoryLimit = 10
deploymentsHistoryLimit = 50
)
// redirectToApp issues a SeeOther redirect to the page for the given
+46 -16
View File
@@ -12,9 +12,9 @@ import (
"sneak.berlin/go/upaas/internal/service/app"
)
// TestAppPageTitleShowsBranch checks that an app's branch can be read from
// the app page title, next to the status badge, without opening the edit page.
func TestAppPageTitleShowsBranch(t *testing.T) {
// TestAppBranchIsShown checks that an app's branch can be read from the app
// page title and from the app list without opening the edit page.
func TestAppBranchIsShown(t *testing.T) {
t.Parallel()
testCtx := setupTestHandlers(t)
@@ -26,21 +26,51 @@ func TestAppPageTitleShowsBranch(t *testing.T) {
})
require.NoError(t, err)
request := httptest.NewRequestWithContext(
t.Context(), http.MethodGet, "/apps/"+createdApp.ID, nil,
)
request = addChiURLParams(request, map[string]string{"id": createdApp.ID})
recorder := httptest.NewRecorder()
t.Run("in the app page title next to the status badge", func(t *testing.T) {
t.Parallel()
testCtx.handlers.HandleAppDetail().ServeHTTP(recorder, request)
request := httptest.NewRequestWithContext(
t.Context(), http.MethodGet, "/apps/"+createdApp.ID, nil,
)
request = addChiURLParams(request, map[string]string{"id": createdApp.ID})
recorder := httptest.NewRecorder()
require.Equal(t, http.StatusOK, recorder.Code)
testCtx.handlers.HandleAppDetail().ServeHTTP(recorder, request)
// The title row runs from the app name heading to the end of its div.
_, afterHeading, found := strings.Cut(recorder.Body.String(), "<h1")
require.True(t, found, "app page has no heading")
require.Equal(t, http.StatusOK, recorder.Code)
titleRow, _, _ := strings.Cut(afterHeading, "</div>")
assert.Contains(t, titleRow, `x-text="statusLabel"`)
assert.Contains(t, titleRow, ">staging</span>")
// The title row runs from the app name heading to the end of its div.
_, afterHeading, found := strings.Cut(recorder.Body.String(), "<h1")
require.True(t, found, "app page has no heading")
titleRow, _, _ := strings.Cut(afterHeading, "</div>")
assert.Contains(t, titleRow, `x-text="statusLabel"`)
assert.Contains(t, titleRow, ">staging</span>")
})
t.Run("in the app list before the repository", func(t *testing.T) {
t.Parallel()
request := httptest.NewRequestWithContext(
t.Context(), http.MethodGet, "/", nil,
)
recorder := httptest.NewRecorder()
testCtx.handlers.HandleDashboard().ServeHTTP(recorder, request)
require.Equal(t, http.StatusOK, recorder.Code)
_, afterRowStart, found := strings.Cut(
recorder.Body.String(), `<tr class="table-row-hover">`,
)
require.True(t, found, "app list has no row for the app")
row, _, _ := strings.Cut(afterRowStart, "</tr>")
branchAt := strings.Index(row, ">staging</td>")
repoAt := strings.Index(row, createdApp.RepoURL)
require.NotEqual(t, -1, branchAt, "branch missing from the app's row")
require.NotEqual(t, -1, repoAt, "repository missing from the app's row")
assert.Less(t, branchAt, repoAt, "branch should come before repository")
})
}
-68
View File
@@ -8,7 +8,6 @@ import (
"strconv"
"strings"
"testing"
"time"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
@@ -1149,73 +1148,6 @@ func TestHandleCancelDeployReturns404ForUnknownApp(t *testing.T) {
assert.Equal(t, http.StatusNotFound, recorder.Code)
}
// TestHandleAppDeploymentsShowsTenNewest verifies the deployments page
// lists only the 10 most recent deployments, newest first.
func TestHandleAppDeploymentsShowsTenNewest(t *testing.T) {
t.Parallel()
testCtx := setupTestHandlers(t)
createdApp := createTestApp(t, testCtx, "deployments-page-app")
// Create 12 deployments, each started one minute after the one before.
firstStart := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
ids := make([]int64, 0, 12)
for idx := range 12 {
deployment := models.NewDeployment(testCtx.database)
deployment.AppID = createdApp.ID
deployment.Status = models.DeploymentStatusSuccess
require.NoError(t, deployment.Save(context.Background()))
_, err := testCtx.database.Exec(
context.Background(),
"UPDATE deployments SET started_at = ? WHERE id = ?",
firstStart.Add(time.Duration(idx)*time.Minute),
deployment.ID,
)
require.NoError(t, err)
ids = append(ids, deployment.ID)
}
request := httptest.NewRequestWithContext(
t.Context(),
http.MethodGet,
"/apps/"+createdApp.ID+"/deployments",
nil,
)
request = addChiURLParams(request, map[string]string{"id": createdApp.ID})
recorder := httptest.NewRecorder()
handler := testCtx.handlers.HandleAppDeployments()
handler.ServeHTTP(recorder, request)
require.Equal(t, http.StatusOK, recorder.Code)
body := recorder.Body.String()
card := func(id int64) string {
return `data-deployment-id="` + strconv.FormatInt(id, 10) + `"`
}
assert.Equal(t, 10, strings.Count(body, `data-deployment-id="`))
// The two oldest are left out.
assert.NotContains(t, body, card(ids[0]))
assert.NotContains(t, body, card(ids[1]))
// The ten newest are shown, newest first.
previous := -1
for idx := len(ids) - 1; idx >= 2; idx-- {
position := strings.Index(body, card(ids[idx]))
require.Greater(t, position, previous,
"deployment %d missing or out of order", ids[idx])
previous = position
}
}
func TestHandleWebhookReturns404ForUnknownSecret(t *testing.T) {
t.Parallel()
+4 -2
View File
@@ -21,12 +21,13 @@
{{if .AppStats}}
<div class="card overflow-hidden">
<div class="overflow-x-auto">
<table class="table">
<thead class="table-header">
<tr>
<th>Name</th>
<th>Repository</th>
<th>Branch</th>
<th>Repository</th>
<th>Status</th>
<th>Last Deploy</th>
<th>Deploys</th>
@@ -41,8 +42,8 @@
{{.App.Name}}
</a>
</td>
<td class="text-gray-500 font-mono text-xs">{{.App.RepoURL}}</td>
<td class="text-gray-500">{{.App.Branch}}</td>
<td class="text-gray-500 font-mono text-xs">{{.App.RepoURL}}</td>
<td>
{{if eq .App.Status "running"}}
<span class="badge-success">Running</span>
@@ -78,6 +79,7 @@
{{end}}
</tbody>
</table>
</div>
</div>
{{else}}
<div class="card">