Widen the app page, double its log heights and move the logs (closes #246)
Check / check (pull_request) Successful in 5m29s
Check / check (pull_request) Successful in 5m29s
The app page's content column is now at most 84rem wide instead of 56rem (max-w-4xl), set inline because the committed Tailwind CSS has no class for that width. The build log and container log boxes are 800px tall instead of 400px. The build log section moves to between the webhook URL and the environment variables, and the container log section moves to directly above the deploy key; nothing else moves. A new handler test renders the app page and checks the width, the section order and both log heights. Disclosure: merged after a rebase that changed only TODO.md; the review gated this change on the next before #234, which touches no template. Model: opus-5-5 Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
This commit was merged in pull request #247.
This commit is contained in:
@@ -20,6 +20,11 @@ regress.
|
|||||||
|
|
||||||
# Completed Steps
|
# 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
|
- 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
|
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
|
end of the build output before that error. upaas refuses to build on a Docker
|
||||||
|
|||||||
@@ -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
@@ -5,7 +5,7 @@
|
|||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
{{template "nav" .}}
|
{{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}}',
|
appId: '{{.App.ID}}',
|
||||||
initialDeploymentId: {{if .LatestDeployment}}{{.LatestDeployment.ID}}{{else}}null{{end}},
|
initialDeploymentId: {{if .LatestDeployment}}{{.LatestDeployment.ID}}{{else}}null{{end}},
|
||||||
initialStatus: '{{.App.Status}}',
|
initialStatus: '{{.App.Status}}',
|
||||||
@@ -54,6 +54,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Deploy Key -->
|
||||||
<div class="card p-6 mb-6">
|
<div class="card p-6 mb-6">
|
||||||
<h2 class="section-title mb-4">Deploy Key</h2>
|
<h2 class="section-title mb-4">Deploy Key</h2>
|
||||||
@@ -101,6 +121,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Environment Variables -->
|
||||||
<div class="card p-6 mb-6" x-data="envVarEditor('{{.App.ID}}')">
|
<div class="card p-6 mb-6" x-data="envVarEditor('{{.App.ID}}')">
|
||||||
<h2 class="section-title mb-4">Environment Variables</h2>
|
<h2 class="section-title mb-4">Environment Variables</h2>
|
||||||
@@ -354,26 +394,6 @@
|
|||||||
</form>
|
</form>
|
||||||
</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: 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 -->
|
<!-- Recent Deployments -->
|
||||||
<div class="card p-6 mb-6">
|
<div class="card p-6 mb-6">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
@@ -413,26 +433,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</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: 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 -->
|
<!-- Danger Zone -->
|
||||||
<div class="card border-2 border-error-500/20 bg-error-50/50 p-6">
|
<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>
|
<h2 class="text-lg font-medium text-error-700 mb-4">Danger Zone</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user