Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d27d730d2 | ||
|
|
b0ab6ee5c0 |
+20
-3
@@ -5,7 +5,17 @@
|
|||||||
# manager and no libc — the attack surface of the running container is the binary.
|
# manager and no libc — the attack surface of the running container is the binary.
|
||||||
|
|
||||||
# --- 1. the web interface ----------------------------------------------------
|
# --- 1. the web interface ----------------------------------------------------
|
||||||
FROM node:22-alpine AS web
|
#
|
||||||
|
# Pinned to the *build* platform, not the target. What this stage produces is
|
||||||
|
# HTML, CSS and JavaScript, which are the same bytes whatever the image will run
|
||||||
|
# on — so building it once and copying it into both architectures is not an
|
||||||
|
# optimisation, it is the correct thing.
|
||||||
|
#
|
||||||
|
# Without the pin, buildx runs this stage once per target, which means running
|
||||||
|
# Node and esbuild under QEMU for arm64. That crashes: exit 132, SIGILL, an
|
||||||
|
# instruction the emulator does not implement, roughly one release in three. It
|
||||||
|
# looks like a broken build and is a broken emulator.
|
||||||
|
FROM --platform=$BUILDPLATFORM node:22-alpine AS web
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY web/ ./
|
COPY web/ ./
|
||||||
@@ -24,7 +34,12 @@ RUN if [ -f package.json ]; then \
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# --- 2. the binary -----------------------------------------------------------
|
# --- 2. the binary -----------------------------------------------------------
|
||||||
FROM golang:1.26-alpine AS build
|
#
|
||||||
|
# Also pinned to the build platform, and cross-compiled instead. Go does that
|
||||||
|
# natively and CGO is already off, so there is nothing to emulate — where
|
||||||
|
# building *in* the target architecture means running the whole Go toolchain
|
||||||
|
# under QEMU for one of the two.
|
||||||
|
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS build
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
@@ -37,11 +52,13 @@ COPY . .
|
|||||||
COPY --from=web /src/build ./cmd/verdande/webbuild
|
COPY --from=web /src/build ./cmd/verdande/webbuild
|
||||||
|
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
|
# Supplied by buildx per target. The default keeps a plain `docker build` working.
|
||||||
|
ARG TARGETARCH=amd64
|
||||||
|
|
||||||
# CGO off is what makes this binary static, and it is only possible because the
|
# CGO off is what makes this binary static, and it is only possible because the
|
||||||
# SQLite driver is pure Go. -trimpath and the empty buildid keep the output
|
# SQLite driver is pure Go. -trimpath and the empty buildid keep the output
|
||||||
# reproducible; -s -w drop the symbol and DWARF tables, which is most of the size.
|
# reproducible; -s -w drop the symbol and DWARF tables, which is most of the size.
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build \
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build \
|
||||||
-tags embedweb \
|
-tags embedweb \
|
||||||
-trimpath \
|
-trimpath \
|
||||||
-ldflags="-s -w -buildid= -X main.version=${VERSION}" \
|
-ldflags="-s -w -buildid= -X main.version=${VERSION}" \
|
||||||
|
|||||||
@@ -804,6 +804,46 @@ test('en rolle kan rettes uden at fjerne personen', async ({ browser, page }) =>
|
|||||||
expect(trouble).toEqual([]);
|
expect(trouble).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The project page on a phone.
|
||||||
|
*
|
||||||
|
* Every other test here runs at desktop width, and this is what that misses: the
|
||||||
|
* header is a flex row of a title and four controls, and 390px does not have room
|
||||||
|
* for them. Without wrapping, the heading was squeezed to its min-content — which
|
||||||
|
* with `overflow-wrap: anywhere` is one character — so a project called
|
||||||
|
* "Skovvænget" rendered as a vertical column of letters while the buttons ran off
|
||||||
|
* the right edge. It looked like a broken font.
|
||||||
|
*/
|
||||||
|
test('projektsiden er brugbar på en telefon', async ({ page }) => {
|
||||||
|
const trouble = watchForTrouble(page);
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// The sidebar is a drawer at this width, so the project is reached through it.
|
||||||
|
await page.getByRole('button', { name: 'Vis menu' }).click();
|
||||||
|
const sidebar = page.getByRole('navigation', { name: 'Hovedmenu' });
|
||||||
|
await sidebar.getByRole('button', { name: 'Nyt projekt' }).click();
|
||||||
|
await sidebar.getByLabel('Projektnavn').fill('Skovvænget');
|
||||||
|
await sidebar.getByLabel('Projektnavn').press('Enter');
|
||||||
|
|
||||||
|
const heading = page.getByRole('heading', { name: 'Skovvænget' });
|
||||||
|
await expect(heading).toBeVisible();
|
||||||
|
|
||||||
|
const box = await heading.boundingBox();
|
||||||
|
// Wide enough to hold the word, and short enough not to be a column of
|
||||||
|
// letters. One line of "Skovvænget" at --text-2xl is nowhere near 200px tall.
|
||||||
|
expect(box.width, 'overskriften er klemt sammen').toBeGreaterThan(120);
|
||||||
|
expect(box.height, 'overskriften er brudt op i én bogstavkolonne').toBeLessThan(120);
|
||||||
|
|
||||||
|
// And nothing sticks out past the right edge of the page.
|
||||||
|
const overflow = await page.evaluate(
|
||||||
|
() => document.documentElement.scrollWidth - document.documentElement.clientWidth
|
||||||
|
);
|
||||||
|
expect(overflow, 'siden kan scrolles vandret').toBeLessThanOrEqual(1);
|
||||||
|
|
||||||
|
expect(trouble).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The MCP connector address must not fall through to the app shell.
|
* The MCP connector address must not fall through to the app shell.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -548,7 +548,10 @@
|
|||||||
ondrop={(e) => onSectionDrop(e, '')}
|
ondrop={(e) => onSectionDrop(e, '')}
|
||||||
>
|
>
|
||||||
<TaskList tasks={unsectioned} projectId={project.id} sectionId="" {canEdit} />
|
<TaskList tasks={unsectioned} projectId={project.id} sectionId="" {canEdit} />
|
||||||
{#if !unsectioned.length}
|
<!-- Only once there are sections to be "without": on a project with none,
|
||||||
|
the label is a heading for the only list there is, and the empty
|
||||||
|
state below already says the project is empty. -->
|
||||||
|
{#if sections.length && !unsectioned.length}
|
||||||
<p class="empty">Uden sektion</p>
|
<p class="empty">Uden sektion</p>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
@@ -664,17 +667,28 @@
|
|||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Wraps, and the title keeps a floor.
|
||||||
|
*
|
||||||
|
* Without both, a phone squeezes the heading to its min-content — which with
|
||||||
|
* `overflow-wrap: anywhere` is *one character*, so "Skovvænget" came out as a
|
||||||
|
* vertical column of letters while the buttons ran off the right edge. Four
|
||||||
|
* controls beside a title is more than 390px has, so they belong on the next
|
||||||
|
* line rather than in a race for the same one.
|
||||||
|
*/
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--s4);
|
gap: var(--s3);
|
||||||
|
flex-wrap: wrap;
|
||||||
margin-bottom: var(--s5);
|
margin-bottom: var(--s5);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: var(--text-2xl);
|
font-size: var(--text-2xl);
|
||||||
flex: 1;
|
/* 8ch rather than 0: the basis is what stops flex shrinking it past the
|
||||||
min-width: 0;
|
point where the wrapping below is the better answer. */
|
||||||
|
flex: 1 1 8ch;
|
||||||
|
min-width: 8ch;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,8 +708,8 @@
|
|||||||
|
|
||||||
/* Matches the heading it stands in for, so the swap does not move the page. */
|
/* Matches the heading it stands in for, so the swap does not move the page. */
|
||||||
.title {
|
.title {
|
||||||
flex: 1;
|
flex: 1 1 8ch;
|
||||||
min-width: 0;
|
min-width: 8ch;
|
||||||
font-size: var(--text-2xl);
|
font-size: var(--text-2xl);
|
||||||
font-weight: 560;
|
font-weight: 560;
|
||||||
letter-spacing: -0.015em;
|
letter-spacing: -0.015em;
|
||||||
|
|||||||
@@ -135,17 +135,21 @@
|
|||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Wraps, for the same reason the project header does: a title and a switcher
|
||||||
|
are more than a phone's width, and the switcher belongs underneath rather
|
||||||
|
than squeezing the heading. */
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--s4);
|
gap: var(--s3);
|
||||||
|
flex-wrap: wrap;
|
||||||
margin-bottom: var(--s5);
|
margin-bottom: var(--s5);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: var(--text-2xl);
|
font-size: var(--text-2xl);
|
||||||
flex: 1;
|
flex: 1 1 8ch;
|
||||||
min-width: 0;
|
min-width: 8ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.views {
|
.views {
|
||||||
|
|||||||
Reference in New Issue
Block a user