Merge next into main: build errors, commit in the footer, wider app page, branch in the title, 10 recent deployments #244

Open
clawbot wants to merge 5 commits from next into main
3 changed files with 122 additions and 41 deletions
Showing only changes of commit 9754b73f27 - Show all commits
+5
View File
@@ -20,6 +20,11 @@ regress.
# Completed Steps
- 2026-09-29: The app page is 50% wider on large screens (84rem instead of
56rem), its build log and container log boxes are twice as tall, the build log
sits between the webhook URL and the environment variables, and the container
log sits above the deploy key (#246).
- 2026-09-29: A failed build now fails the deploy with the build's own error
instead of a later "failed to inspect image", and the deployment log shows the
end of the build output before that error. upaas refuses to build on a Docker
+76
View File
@@ -0,0 +1,76 @@
package handlers_test
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sneak.berlin/go/upaas/internal/service/app"
)
// TestAppPageLayout checks the app page's width, the order of its sections,
// and the height of its two log boxes.
func TestAppPageLayout(t *testing.T) {
t.Parallel()
testCtx := setupTestHandlers(t)
createdApp, err := testCtx.appSvc.CreateApp(t.Context(), app.CreateAppInput{
Name: "layout-app",
RepoURL: "git@example.com:user/layout-app.git",
})
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()
testCtx.handlers.HandleAppDetail().ServeHTTP(recorder, request)
require.Equal(t, http.StatusOK, recorder.Code)
body := recorder.Body.String()
_, afterMain, found := strings.Cut(body, "<main")
require.True(t, found, "app page has no main element")
mainTag, _, _ := strings.Cut(afterMain, ">")
assert.Contains(t, mainTag, "max-width: 84rem;")
sectionTitles := []string{
"Container Logs",
"Deploy Key",
"Webhook URL",
"Last Deployment Build Logs",
"Environment Variables",
"Docker Labels",
"Volume Mounts",
"Port Mappings",
"Recent Deployments",
"Danger Zone",
}
previousIndex := -1
for _, title := range sectionTitles {
index := strings.Index(body, ">"+title+"</h2>")
require.NotEqual(t, -1, index, "app page has no %q section", title)
assert.Greater(t, index, previousIndex, "%q section is out of order", title)
previousIndex = index
}
for _, logBox := range []string{"containerLogsWrapper", "buildLogsWrapper"} {
_, afterRef, found := strings.Cut(body, `x-ref="`+logBox+`"`)
require.True(t, found, "app page has no %s", logBox)
logBoxTag, _, _ := strings.Cut(afterRef, ">")
assert.Contains(t, logBoxTag, "max-height: 800px;", logBox)
}
}
+41 -41
View File
@@ -5,7 +5,7 @@
{{define "content"}}
{{template "nav" .}}
<main class="max-w-4xl mx-auto px-4 py-8" x-data="appDetail({
<main class="mx-auto px-4 py-8" style="max-width: 84rem;" x-data="appDetail({
appId: '{{.App.ID}}',
initialDeploymentId: {{if .LatestDeployment}}{{.LatestDeployment.ID}}{{else}}null{{end}},
initialStatus: '{{.App.Status}}',
@@ -54,6 +54,26 @@
</div>
</div>
<!-- Container Logs -->
<div class="card p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="section-title">Container Logs</h2>
<span x-bind:class="containerStatusBadgeClass" x-text="containerStatusLabel"></span>
</div>
<div class="relative">
<div x-ref="containerLogsWrapper" class="bg-gray-900 rounded-lg p-4 overflow-y-auto" style="max-height: 800px;">
<pre class="text-gray-100 text-xs font-mono whitespace-pre-wrap break-words m-0" x-text="containerLogs"></pre>
</div>
<button
x-show="!_containerAutoScroll"
x-transition
@click="_containerAutoScroll = true; Alpine.store('utils').scrollToBottom($refs.containerLogsWrapper)"
class="absolute bottom-2 right-4 bg-primary-600 hover:bg-primary-700 text-white text-xs px-3 py-1 rounded-full shadow-lg opacity-90 hover:opacity-100 transition"
title="Scroll to bottom"
>↓ Follow</button>
</div>
</div>
<!-- Deploy Key -->
<div class="card p-6 mb-6">
<h2 class="section-title mb-4">Deploy Key</h2>
@@ -101,6 +121,26 @@
</div>
</div>
<!-- Last Deployment Build Logs -->
<div class="card p-6 mb-6" x-show="showBuildLogs" x-cloak>
<div class="flex items-center justify-between mb-4">
<h2 class="section-title">Last Deployment Build Logs</h2>
<span x-bind:class="buildStatusBadgeClass" x-text="buildStatusLabel"></span>
</div>
<div class="relative">
<div x-ref="buildLogsWrapper" class="bg-gray-900 rounded-lg p-4 overflow-y-auto" style="max-height: 800px;">
<pre class="text-gray-100 text-xs font-mono whitespace-pre-wrap break-words m-0" x-text="buildLogs"></pre>
</div>
<button
x-show="!_buildAutoScroll"
x-transition
@click="_buildAutoScroll = true; Alpine.store('utils').scrollToBottom($refs.buildLogsWrapper)"
class="absolute bottom-2 right-4 bg-primary-600 hover:bg-primary-700 text-white text-xs px-3 py-1 rounded-full shadow-lg opacity-90 hover:opacity-100 transition"
title="Scroll to bottom"
>↓ Follow</button>
</div>
</div>
<!-- Environment Variables -->
<div class="card p-6 mb-6" x-data="envVarEditor('{{.App.ID}}')">
<h2 class="section-title mb-4">Environment Variables</h2>
@@ -354,26 +394,6 @@
</form>
</div>
<!-- Container Logs -->
<div class="card p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="section-title">Container Logs</h2>
<span x-bind:class="containerStatusBadgeClass" x-text="containerStatusLabel"></span>
</div>
<div class="relative">
<div x-ref="containerLogsWrapper" class="bg-gray-900 rounded-lg p-4 overflow-y-auto" style="max-height: 400px;">
<pre class="text-gray-100 text-xs font-mono whitespace-pre-wrap break-words m-0" x-text="containerLogs"></pre>
</div>
<button
x-show="!_containerAutoScroll"
x-transition
@click="_containerAutoScroll = true; Alpine.store('utils').scrollToBottom($refs.containerLogsWrapper)"
class="absolute bottom-2 right-4 bg-primary-600 hover:bg-primary-700 text-white text-xs px-3 py-1 rounded-full shadow-lg opacity-90 hover:opacity-100 transition"
title="Scroll to bottom"
>↓ Follow</button>
</div>
</div>
<!-- Recent Deployments -->
<div class="card p-6 mb-6">
<div class="flex items-center justify-between mb-4">
@@ -413,26 +433,6 @@
</template>
</div>
<!-- Last Deployment Build Logs -->
<div class="card p-6 mb-6" x-show="showBuildLogs" x-cloak>
<div class="flex items-center justify-between mb-4">
<h2 class="section-title">Last Deployment Build Logs</h2>
<span x-bind:class="buildStatusBadgeClass" x-text="buildStatusLabel"></span>
</div>
<div class="relative">
<div x-ref="buildLogsWrapper" class="bg-gray-900 rounded-lg p-4 overflow-y-auto" style="max-height: 400px;">
<pre class="text-gray-100 text-xs font-mono whitespace-pre-wrap break-words m-0" x-text="buildLogs"></pre>
</div>
<button
x-show="!_buildAutoScroll"
x-transition
@click="_buildAutoScroll = true; Alpine.store('utils').scrollToBottom($refs.buildLogsWrapper)"
class="absolute bottom-2 right-4 bg-primary-600 hover:bg-primary-700 text-white text-xs px-3 py-1 rounded-full shadow-lg opacity-90 hover:opacity-100 transition"
title="Scroll to bottom"
>↓ Follow</button>
</div>
</div>
<!-- Danger Zone -->
<div class="card border-2 border-error-500/20 bg-error-50/50 p-6">
<h2 class="text-lg font-medium text-error-700 mb-4">Danger Zone</h2>