From 6915be534d8933b56d9a3de2782bff7ae2c70f54 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Fri, 31 Jul 2026 19:33:46 +0200 Subject: [PATCH] test(release): verify client feedback rollout --- docs/client-feedback-deployment-readiness.md | 275 +++++++++++++++++++ tests/int/publication-contract.int.spec.ts | 19 ++ 2 files changed, 294 insertions(+) create mode 100644 docs/client-feedback-deployment-readiness.md diff --git a/docs/client-feedback-deployment-readiness.md b/docs/client-feedback-deployment-readiness.md new file mode 100644 index 0000000..3c938fb --- /dev/null +++ b/docs/client-feedback-deployment-readiness.md @@ -0,0 +1,275 @@ +# Client feedback rollout: deployment readiness + +Verification date: 2026-07-31 + +## Decision + +Tasks 1–17 are ready for a coordinated schema-changing deployment. Local code, +CMS ownership, isolated database migrations, the full serial Vitest integration +suite, the production webpack build, generated sitemaps, and public/draft +behavior have been verified. + +The rollout is not yet complete in production. Production is still serving +revision `740e302c58f8`, before Tasks 1–17, and the production-write gates listed +below remain intentionally blocked. No production database, media, publication +state, or deployment was changed during Task 18. + +## Commit sequence + +The clean root worktree and this verification worktree both started at +`07e9458aaf2796f5f48a69302acd26914032b879`. The root `main` history contains +exactly these 17 Task 1–17 commits after production revision `740e302c58f8`: + +| Task | Commit | Subject | +| --- | --- | --- | +| 1 | `fec1bab2e521d38af9de3c7c3c315c0da91795e7` | `fix(cms): enforce public publication contract` | +| 2 | `b18bbbe36ad395cb26011d6032e06f460ded3f2a` | `fix(cms): filter unpublished relationships` | +| 3 | `8dc17c0fea9420b2854a074d3924d4341147f04b` | `fix(cms): remove unpublished content fallbacks` | +| 4 | `ab47571139a177339a002ff62336a61f7a44d4c6` | `refactor(cms): remove obsolete page fields` | +| 5 | `e3c47c4500150be0fb40caa6cb4ae2619f61789c` | `feat(cms): add network page video upload` | +| 6 | `c2dc42f243e58c7cb81e2e23e2aa2e34110ca4fc` | `feat(cms): add press contact image` | +| 7 | `ebecf66e6fcb8514d5206bfc80ad36f4c9939e4e` | `fix(participation): repair process timeline` | +| 8 | `30efb4b36ee7c0187c0274f319679846287ae3cb` | `feat(network): add responsive sponsor hierarchy` | +| 9 | `95037e99089837a64a4315f6d2a467e8e5be61f5` | `feat(cms): clarify sponsor ordering` | +| 10 | `adda9ddbd07a7bb04947c349a2117dd31879d190` | `fix(home): slow sponsor ticker` | +| 11 | `f465839af0e49fa5021272c0a85786a90a763589` | `fix(home): honor phase headline line breaks` | +| 12 | `5b62abf8d4b205d8603f3a0d34393d47d30412e8` | `fix(home): rebalance application form split` | +| 13 | `d7a720460f542fc6c34ad3bd46d2eb5be1e2be6e` | `docs(home): verify application panel color` | +| 14 | `4bb265dac1b7862db1e468d63126a07e9eaa8aec` | `fix(participation): clarify eligibility hierarchy` | +| 15 | `3c28f6ff9c53486b87c2d45ffb57e35bc5a025d7` | `fix(about): rebalance relevance section` | +| 16 | `0a19abd0b080095748f1de28dfe829da62316e96` | `fix(about): align facts and caption` | +| 17 | `07e9458aaf2796f5f48a69302acd26914032b879` | `fix(seo): correct sitemap routes` | + +Task 18 is the single commit containing this report and the regression-test +cleanup described below. Its exact hash is recorded in the Task 18 handoff +because a commit cannot embed its own hash. + +No Task 1–17 integration is missing, and no unrelated tracked changes were +present at the start of Task 18. + +## Automated verification + +All database-backed checks used copies of the current local `content.db`. The +source database SHA-256 was +`fb7de5d9f1fcf3a7c27494efe7f3111cba4173bc986a38ea62c9cf6d5840425e` and was +never migrated or otherwise mutated. + +### Integration tests + +- Focused publication, relationship, sitemap, CMS-field, media, responsive + section, timeline, sponsor, press, home, and about tests passed. +- Complete serial suite against a freshly copied and migrated database: + **21 files, 92 tests, all passed**. +- Serial command shape used to avoid SQLite parallel-write contention: + + ```bash + DATABASE_URL=file:/absolute/path/to/isolated.db \ + PAYLOAD_SECRET=task18-isolated-verification \ + PAYLOAD_MIGRATING=true \ + SITE_URL=http://127.0.0.1:3100 \ + ./node_modules/.bin/vitest run \ + --config ./vitest.config.mts \ + --maxWorkers=1 \ + --no-file-parallelism \ + --hookTimeout=30000 + ``` + +`PAYLOAD_MIGRATING=true` is required for tests against an explicitly migrated +database so Payload does not also attempt its development schema-push path. + +### Task 18 regression fix + +`tests/int/publication-contract.int.spec.ts` created unique publication fixtures +but deleted only their media record. A second run against the same isolated +database therefore failed unique `slug` and `stableId` validation. Task 18 now +deletes every created document before deleting the media record. The focused +suite passed twice consecutively afterward, and the complete 92-test serial +suite remained green. + +### Static and build checks + +- `./node_modules/.bin/tsc --noEmit --incremental false`: passed. +- `payload generate:types`: passed; `src/payload-types.ts` was byte-for-byte + unchanged. +- `git diff --check`: passed. +- Task 18 changed-file Prettier check: passed. +- Production webpack build against the migrated database: + `next build --webpack`: passed and generated all expected application, + Payload, preview, and sitemap routes. +- Production-safe sitemap postbuild with + `NEXT_PUBLIC_SERVER_URL=https://srv1.bayerischer-mittelstandspreis.de`: + passed. Generated `robots.txt` and `sitemap.xml` contain only the canonical + HTTPS origin. +- Local generated and dynamic sitemap endpoints returned `200`; `/`, the + representative article, event, and winner URLs were present, no localhost or + placeholder origin was present, and the unpublished Task 18 fixture was + absent. + +## Migration verification + +Initial status on the copied current database was 5 recorded migrations and 18 +pending migrations. The isolated upgrade completed all pending migrations and +ended with all 23 repository migrations recorded. `PRAGMA integrity_check` +returned `ok`. + +Every migration introduced by Tasks 1–17 was additionally verified through its +own copied-database down/up cycle, with integrity checks after both directions: + +- `20260731_143000_remove_press_record_fallbacks` +- `20260731_160000_focused_payload_cleanup` +- `20260731_170000_netzwerk_greeting_video` +- `20260731_180000_press_contact_image` +- `20260731_190000_responsive_sponsor_hierarchy` +- `20260731_200000_home_phase_headline_line_break` + +All six targeted down/up cycles passed. The real `content.db` was never used as +a migration target. + +## Browser verification + +The production build was served locally against the isolated migrated database +and the copied local media directory. Anonymous and authenticated preview checks +used separate browser state. + +### Route and viewport matrix + +The following routes were checked at approximately 1440, 1024, 768, and an +exact 390 CSS pixels: + +- `/` +- `/teilnahme` +- `/der-bmp` +- `/netzwerk` +- `/presse` +- `/presse/events/nominierten-auswahl-2026` +- `/presse/blog/goldene-bavaria` +- `/preistraeger/agrarquant-gmbh` + +All 32 combinations rendered their expected CMS-owned heading and substantive +main content with zero broken images. There was no page-level horizontal +overflow except for the pre-existing event-detail issue below. + +Specific checks also passed: + +- Homepage phase heading rendered the CMS newline as two lines. +- Shared `#bewerben` section computed to `rgb(36, 54, 106)` (`#24366A`). +- `/teilnahme` timeline advanced from `01 / 04` through `04 / 04`; all four + desktop/tablet steps were reachable, the 1024px section was 667px tall, and + the next-section gap was `0`. At 390px all four mobile headings rendered in + document order, controls were hidden, and the next-section gap was `0`. +- Eligibility groups and the dates transition remained in the intended mobile + scan order. +- `/der-bmp` relevance layout was split on desktop; all three facts were + centered and the image caption had a visible dark controlled background. +- `/netzwerk` rendered tier bands with current counts 2 / 2 / 8 in authoritative + order. With no Netzwerk video selected, no video action rendered. +- `/presse` rendered the safe `copy-only` contact layout with no contact image + selected. + +### Anonymous public versus authenticated preview + +An isolated draft press article was created with slug +`task18-preview-fixture`. + +- Anonymous direct browsing returned the 404 page and exposed neither the + fixture title nor body. +- After authenticating an isolated Payload user and entering through + `/next/preview`, the same path rendered the draft title. +- Publication and relationship integration tests independently prove this + boundary for pages, winners, jury members, partners, events, and posts. + +## Read-only production smoke + +No production login or write was performed. Browser and HTTP checks produced: + +| Production route | Result | +| --- | --- | +| `/` | `200`, expected main content, no broken images | +| `/teilnahme` | `200`, expected main content, no broken images | +| `/der-bmp` | `200`, expected main content, no broken images | +| `/netzwerk` | `200`, expected main content, no broken images | +| `/presse` | `200`, expected main content, no broken images | +| `/presse/blog/goldene-bavaria` | `200`, expected article detail | +| `/presse/events/nominierten-auswahl-2026` | `404` before rollout | +| `/preistraeger/agrarquant-gmbh` | `404` before rollout | +| `/sitemap.xml` | `200` | +| `/pages-sitemap.xml` | `200` | +| `/posts-sitemap.xml` | `200` | +| `/robots.txt` | `200` | + +The read-only VPS status script confirmed deployed revision `740e302c58f8`, +which is exactly the parent of Task 1. Further container/status output requires +interactive server `sudo` and was not attempted. + +The current production sitemap is therefore expectedly stale: it still emits +`/startseite`, emits three `/posts/` URLs that each return `404`, and has +no event or winner detail entries. The corrected local sitemap cannot be judged +live until the verified commit sequence is deployed. + +## Known tooling and pre-existing limitations + +- Repo-wide ESLint does not reach file evaluation because the current flat + configuration throws `Cannot redefine plugin "@typescript-eslint"`. +- A rollout-wide Prettier audit reports 41 already-committed Task 1–17 files as + nonconforming. Task 18's changed files pass Prettier; bulk reformatting was + intentionally excluded from this release-verification commit. +- The installed global pnpm is `11.9.0`, while this repo requires pnpm `^9` or + `^10`. `pnpm vps:status` is blocked by the engine check; invoking the + read-only script directly reached production and confirmed the revision. +- The in-app browser enforces a 480px minimum CSS viewport. The exact 390px + matrix was completed through the installed Chrome browser surface instead. +- In-app navigation to local XML endpoints was blocked by the browser client; + read-only HTTP checks verified all four sitemap/robots endpoints instead. +- The representative event detail overflows horizontally at exactly 768px: + the pre-existing `EventDetail.tsx` layout combines 80px side padding, a 64px + gap, and a fixed 400px sidebar. Tasks 1–17 did not change this component, so + Task 18 did not expand scope to redesign it. The same route has no overflow + at 390, 1024, or 1440px. + +## Required production procedure and blocked gates + +These actions are intentionally not performed by Task 18. They require explicit +production-write coordination and a supported pnpm `^9` or `^10` runtime. + +1. Obtain explicit production-write authorization and identify the exact commit + to deploy. +2. Create the documented server-side SQLite backup: + + ```bash + pnpm vps:backup-db + ``` + +3. Deploy the committed schema-changing rollout with migrations enabled: + + ```bash + RUN_MIGRATIONS=1 pnpm deploy:vps + ``` + + Per `docs/vps-deployment.md`, this mode also creates a pre-migration database + copy before `pnpm payload migrate` runs in the one-off app container. + +4. Confirm the deployed revision, container health, database/media state, and + logs: + + ```bash + pnpm vps:status + ``` + +5. Obtain the client-supplied Netzwerk video, upload/select it through Payload + on the Netzwerk page, and verify that changing it does not affect the + homepage video. No video asset was supplied in this task. +6. Obtain the approved press-contact image, upload/select it with useful alt + text through Payload on the Presse page, and verify the two-column desktop + and stacked mobile layouts. No image asset was supplied in this task. +7. Identify approved disposable production records before testing Unpublish and + Republish. Record IDs and `_status`, verify anonymous list/detail removal, + verify authenticated preview visibility, republish, and confirm restoration. + No disposable record was approved in this task. +8. Re-run the production route, sitemap, robots, media, responsive, and + publication smoke matrix after deployment. In particular, every URL emitted + by the corrected sitemap must return `200`. + +Blocked gates at handoff are therefore: client Netzwerk video, client press +image, approved production unpublish record(s), supported deployment tooling, +interactive server privileges where required, and explicit production-write +authorization. diff --git a/tests/int/publication-contract.int.spec.ts b/tests/int/publication-contract.int.spec.ts index f87181a..695cc75 100644 --- a/tests/int/publication-contract.int.spec.ts +++ b/tests/int/publication-contract.int.spec.ts @@ -53,6 +53,7 @@ const lexicalDocument = { } let mediaID: number +const cleanup: Array<() => Promise> = [] const publicationCases: PublicationCase[] = [ { @@ -139,6 +140,10 @@ describe('draft-enabled collection publication contract', () => { }) afterAll(async () => { + for (const removeFixture of cleanup.reverse()) { + await removeFixture() + } + if (mediaID) { await payload.delete({ collection: 'media', id: mediaID }) } @@ -170,6 +175,20 @@ describe('draft-enabled collection publication contract', () => { data: data(`${collection}-unpublished`), draft: false, }) + cleanup.push( + () => + payload.delete({ + collection, + context: { disableRevalidate: true }, + id: published.id, + }), + () => + payload.delete({ + collection, + context: { disableRevalidate: true }, + id: unpublished.id, + }), + ) const storedDraft = await payload.update({ collection,