From 74d6fd86d4df8502aeecd8c72956380ff2384ebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wr=C3=B3blewski?= Date: Sat, 12 Sep 2026 09:08:39 +0200 Subject: [PATCH 1/2] Task index: #66 done, two new tasks, and #113 raised (#66, #113, #172, #173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #66 landed with #170: the logo above the auth cards, the place a phone could not add, and both lists put in order by hand. Two things it produced rather than closed: - #173, Dawid's own words on seeing it work — the order is right, the movement is not, and for now it stays. - #172, from the security review — the pool waits for ever and no statement is ever cut off, which is every route rather than that one. And #113 is raised, with the reason written down: a preview running the pull request's image against dev's database means any pull request carrying a migration looks broken on its own preview, exactly when the preview exists to be looked at. Twice now. Co-Authored-By: Claude Opus 5 --- tasks/plan.md | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/tasks/plan.md b/tasks/plan.md index 964b066..2af44c4 100644 --- a/tasks/plan.md +++ b/tasks/plan.md @@ -79,11 +79,24 @@ in under 5 minutes (manual walkthrough); e2e green. - ~~[#31](https://github.com/Devski/platform-lite/issues/31) PR preview deployments on the dev instance~~ — **done 05.09.2026**: `pr-.dev.architektow3d.pl`, named sites over HTTP-01 (no wildcard certificate, so no DNS plugin), shared dev database and a `pr-/` key prefix. Two at a time — the instance has one core and no swap. Previews never send e-mail. - [#113](https://github.com/Devski/platform-lite/issues/113) PR previews: a database cloned - from dev per preview, not the shared one (`deployment`). Filed 09.09.2026 when the preview - of #112 answered with a server error: it ran the pull request's image against dev's - schema, and previews never migrate. Sequenced after the R360 trial on dev. + from dev per preview, not the shared one (`deployment`). **Raised in priority 12.09.2026** + after it cost a second review cycle. Filed 09.09.2026 when the preview of #112 answered + with a server error: it runs the pull request's image against dev's database, and previews + never migrate. #170 hit it again — every page listing works answered 500 on that preview + because the migration adding `works.position` had not run there. It is not a flake but a + rule: **any pull request carrying a migration looks broken on its own preview**, exactly + when the preview exists to be looked at. The workaround both times was to apply the + migration to dev by hand first, which is only safe while migrations stay expand-only (G6) + and someone is there to reason about it. - [#111](https://github.com/Devski/platform-lite/issues/111) Preview cleanup loses the race with a CI run still in flight, and the orphan blocks the two-preview cap (`bug`). +- [#172](https://github.com/Devski/platform-lite/issues/172) The database has no deadlines + (`infra`, `deployment`). Found 12.09.2026 in the security review of #66: the pool is built + with pg's defaults, so `connectionTimeoutMillis` is 0 — a request waiting for a free + connection waits for ever — and neither `statement_timeout` nor `lock_timeout` is set + anywhere, on either side. Every limit in this system is a rate limit on the way IN; past + it, nothing bounds how long a request holds a connection or a lock, so contention queues + silently instead of failing with something to read. - [#119](https://github.com/Devski/platform-lite/issues/119) The dev instance keeps every image it ever pulled (`infra`). Filed 09.09.2026 when its root filesystem reached 100%: 129 images, 21.8 GB, three of them in use. Previews stopped starting at all, and dev's @@ -290,6 +303,21 @@ recorded so it is not rediscovered later. - ~~[#36](https://github.com/Devski/platform-lite/issues/36) The public profile showed the user's e-mail address as their name~~ — **done 05.09.2026**: registration stopped inventing a name from the address; onboarding asks for one in two steps and derives the address from it. A migration cleared what the old flow wrote, keeping handles — they may already have been shared. Found two faults of my own on the way, both recorded on the issue and generalised as #39. - [#39](https://github.com/Devski/platform-lite/issues/39) Make the UI airtight against what the backend and the database will accept (`enhancement`) — opened 05.09.2026: a rule can live in the form, the API schema and a database constraint, and nothing keeps the three in agreement. Two instances on one screen in #36: a submit the form could not know would fail, and a `CHECK` nothing above the database could see — both surfacing as "try again". - [#44](https://github.com/Devski/platform-lite/issues/44) Where personal data lives: which of it is sensitive, and does it belong in its own store (`compliance`) — opened 06.09.2026, four questions to answer in writing before production. The name is public by design; the sensitive thing is its LINK to the private address. Also carries four gaps found while surveying: disk encryption unverified, no dev backups, session tokens in plaintext, recipient address possibly reaching a log line. +- ~~[#66](https://github.com/Devski/platform-lite/issues/66) Small things to polish~~ — + **done 12.09.2026** (PR #170), four of them: the logo above the auth cards stopped being a + link (a visitor part-way through making an account was being offered a way out, and after + verification that link looped to the page it was on); Tab commits a typed place, because a + phone's keyboard offers "next" where a desktop offers Enter and the typed text went with + the focus; and both the places and the works can be dragged into order, one piece of + machinery for a wrapped row of chips and a grid of cards. Works needed a `position` column + — expand-only, backfilled from `created_at`, so nothing shuffled. The reviews found what + the tests could not: a refused save that resurrected a deleted work, saves racing each + other under a repeating arrow key, and grips whose keys a screen reader would never guess. +- [#173](https://github.com/Devski/platform-lite/issues/173) Dragging a list should look like + dragging (`ux`). Dawid, 12.09.2026, on seeing #66 work: the order is right, the movement is + not — the held item fades in place and everything jumps on the drop. For now it stays. The + boxes needed for it are already measured at pointerdown, so this is a transform and a + transition rather than a rewrite. - Three from Dawid on 11.09.2026, deliberately outside the R360 milestone. One is already settled: ~~[#151](https://github.com/Devski/platform-lite/issues/151) an orbit from a RAR archive~~ — **dropped 12.09.2026** at his word, we do not support RAR for now; a zip is From bb7fd2bd47fa4818c8f71f0e9e9d260e70130358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wr=C3=B3blewski?= Date: Sat, 12 Sep 2026 12:40:03 +0200 Subject: [PATCH 2/2] The reorder tests read the page before React has drawn it (#66) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `main` went red on the test I wrote, not on the product: `works-order` compared the list of names with a plain expect straight after a key press. That is a snapshot, not a retrying assertion, so it asked the page what it looked like before React had finished putting it there — green on a quiet machine, red on CI. Both specs use `toHaveText` now, which waits. Second one in the same test, and this one my own doing: the order is sent once the moving stops (300 ms), so the reload that follows the second arrow press was racing the request and would have cancelled it in flight. It is waited for, like the first. Nothing in the product changed. Co-Authored-By: Claude Opus 5 --- e2e/db/profile-sections.spec.ts | 14 ++++---------- e2e/db/works-order.spec.ts | 33 +++++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/e2e/db/profile-sections.spec.ts b/e2e/db/profile-sections.spec.ts index a537829..27af050 100644 --- a/e2e/db/profile-sections.spec.ts +++ b/e2e/db/profile-sections.spec.ts @@ -192,7 +192,7 @@ test("the places are put in order and stay in it, for the owner and for a visito await page.getByRole("button", { name: "Edytuj profil" }).click(); const chips = page.locator("li:has(button[aria-label^='Przesuń miejsce'])"); - expect(await chips.allInnerTexts()).toEqual(["Warszawa", "Nowa Wieś"]); + await expect(chips).toHaveText(["Warszawa", "Nowa Wieś"]); // The arrow keys on the grip, which is the whole of this for anyone not // using a mouse — and, unlike a drag, it says what it did out loud. @@ -205,16 +205,13 @@ test("the places are put in order and stay in it, for the owner and for a visito await expect( page.getByText("Miejsce Nowa Wieś jest teraz na pozycji 1"), ).toHaveCount(1); - expect(await chips.allInnerTexts()).toEqual(["Nowa Wieś", "Warszawa"]); + await expect(chips).toHaveText(["Nowa Wieś", "Warszawa"]); await page.reload(); const section = page.locator("section", { has: page.getByRole("heading", { name: "Siedziba i obszar działania" }), }); - expect(await section.locator("li").allInnerTexts()).toEqual([ - "Nowa Wieś", - "Warszawa", - ]); + await expect(section.locator("li")).toHaveText(["Nowa Wieś", "Warszawa"]); const visitor = await browser.newPage({ locale: "pl-PL" }); try { @@ -224,10 +221,7 @@ test("the places are put in order and stay in it, for the owner and for a visito name: "Siedziba i obszar działania", }), }); - expect(await theirs.locator("li").allInnerTexts()).toEqual([ - "Nowa Wieś", - "Warszawa", - ]); + await expect(theirs.locator("li")).toHaveText(["Nowa Wieś", "Warszawa"]); } finally { await visitor.close(); } diff --git a/e2e/db/works-order.spec.ts b/e2e/db/works-order.spec.ts index 13bc524..8370e7b 100644 --- a/e2e/db/works-order.spec.ts +++ b/e2e/db/works-order.spec.ts @@ -35,9 +35,15 @@ const THIRD = "Dom w lesie"; let page: Page; let identity: Identity; -/** The work names as the page lists them, top to bottom. */ -async function namesOn(target: Page): Promise { - return target.locator("article h3").allInnerTexts(); +/** + * The work names as the page lists them, top to bottom — as a retrying + * assertion, not a snapshot. Reading the names straight after a key press + * asks the page what it looks like before React has finished putting it + * there: green on a quiet machine, red about one run in three on CI, and the + * difference is not the product. + */ +function namesOn(target: Page) { + return expect(target.locator("article h3")); } test.beforeAll(async ({ browser }) => { @@ -55,7 +61,7 @@ test.afterAll(async () => { test("the grips appear only while editing", async () => { await page.goto(`/${identity.handle}`); - expect(await namesOn(page)).toEqual([FIRST, SECOND, THIRD]); + await namesOn(page).toHaveText([FIRST, SECOND, THIRD]); await expect( page.getByRole("button", { name: /^Przesuń realizację/ }), ).toHaveCount(0); @@ -84,14 +90,21 @@ test("the arrow keys on a grip move a work, and the order survives a reload", as await expect( page.getByText(`Realizacja ${THIRD} jest teraz na pozycji 2`), ).toHaveCount(1); - expect(await namesOn(page)).toEqual([FIRST, THIRD, SECOND]); + await namesOn(page).toHaveText([FIRST, THIRD, SECOND]); // The keyboard stays on the work it moved, or a second press would move // whatever took its place. + const savedAgain = page.waitForResponse((response) => + response.url().includes("/api/works/order"), + ); await page.keyboard.press("ArrowUp"); - expect(await namesOn(page)).toEqual([THIRD, FIRST, SECOND]); + await namesOn(page).toHaveText([THIRD, FIRST, SECOND]); + // Waited for before the reload, not out of tidiness: the order is sent + // once the moving stops, so a reload racing that timer would cancel the + // request in flight and the test would be about the wrong thing. + expect((await savedAgain).status()).toBe(200); await page.reload(); - expect(await namesOn(page)).toEqual([THIRD, FIRST, SECOND]); + await namesOn(page).toHaveText([THIRD, FIRST, SECOND]); }); test("a work dragged by its grip lands where it was dropped", async () => { @@ -121,16 +134,16 @@ test("a work dragged by its grip lands where it was dropped", async () => { await page.mouse.up(); expect((await saved).status()).toBe(200); - expect(await namesOn(page)).toEqual([SECOND, THIRD, FIRST]); + await namesOn(page).toHaveText([SECOND, THIRD, FIRST]); await page.reload(); - expect(await namesOn(page)).toEqual([SECOND, THIRD, FIRST]); + await namesOn(page).toHaveText([SECOND, THIRD, FIRST]); }); test("a visitor sees the owner's order", async ({ browser }) => { const visitor = await browser.newPage({ locale: "pl-PL" }); try { await visitor.goto(`/${identity.handle}`); - expect(await namesOn(visitor)).toEqual([SECOND, THIRD, FIRST]); + await namesOn(visitor).toHaveText([SECOND, THIRD, FIRST]); // Nothing to take hold of on someone else's profile. await expect( visitor.getByRole("button", { name: /^Przesuń realizację/ }),