3. Public pages that PHP Journal + the current theme rendered have a named successor on Go; sitemap is in if the site still needs it.
4. The new code has unit tests, delivered in the phase's last plan.
**Plans:**2/5 plans executed
**Plans:**3/5 plans executed
Plans:
**Wave 1**
@@ -969,7 +969,7 @@ Plans:
- [x] 16-02-PLAN.md — vue-grzybyfunkcjonalne-app: SSR site on the layer with the PHP URL map, homepage/menu/FAQ/kontakt content port, GA via runtime config; submodule in the host
**Wave 3***(blocked on Wave 2 completion)*
- [] 16-03-PLAN.md — sm-grzybyfunkcjonalne-app: /gfadmin, app plugin with featured-image and category-content commands, migration runbook, two-process deploy files, urls.txt and local rehearsal
- [x] 16-03-PLAN.md — sm-grzybyfunkcjonalne-app: /gfadmin, app plugin with featured-image and category-content commands, migration runbook, two-process deploy files, urls.txt and local rehearsal
**Wave 4***(blocked on Wave 3 completion)*
- [ ] 16-04-PLAN.md — Staging deploy on the PHP site's server (human checkpoint) and smoke over every old URL
Phase: 16 (grzybyfunkcjonalne.pl on reusable blog views) — EXECUTING
Plan: 3 of 5
Plan: 4 of 5
Status: Ready to execute
Last activity: 2026-10-09 - Completed quick task 261009-mys: subtitle, avatar and My account page for backend users
@@ -196,6 +196,7 @@ Progress: [██████████] 100%
| Phase 14.3 P04 | 19min | 3 tasks | 32 files |
| Phase 16 P01 | 1h 34m | 4 tasks | 55 files |
| Phase 16 P02 | 16 min | 3 tasks | 44 files |
| Phase 16 P03 | 22 min | 3 tasks | 32 files |
## Accumulated Context
@@ -628,6 +629,10 @@ Recent decisions affecting current work:
- [Phase 16]: Under routeRules swr the site app records the visitor address in event.context (Nitro request hook) and overrides useJournalApi, because Nitro's cached handler strips request headers; the proper fix belongs in vue-journal-layer
- [Phase 16]: Site page URLs answer errors with the HTML error page even for fetch-like requests (server/plugins/html-errors.ts); /api/, /_ and .json paths keep Nuxt JSON errors
- [Phase 16]: Site rich-text copy stays in pl.json (i18n compilation.strictMessage false); contact data lives in app/data/contact.ts
- [Phase 16]: Plugin commands in a summer-build host open and publish the database themselves (lookupDB via lagoon); the journal import runs as grzyby:import-posts because the generated main.go publishes no DB for plugin commands
- [Phase 16]: migrate.sh imports from a copy of the PHP export with empty translations [] arrays normalized to {}; the importer fix is deferred to sm-journal-plugin
- [Phase 16]: The URL rehearsal gives each URL its own X-Forwarded-For visitor (203.0.113.N) so ~90 SSR pages stay under Go's 120/min per-visitor journal limit
- [Phase 16]: The staging nginx image location declares its own types map and matches extensions case-insensitively so images keep an image type under nosniff
### Pending Todos
@@ -697,6 +702,6 @@ Items acknowledged and carried forward from previous milestone close:
- "Plugin commands in a summer-build host open and publish the database themselves, because the generated main.go publishes none for them; the journal import runs as grzyby:import-posts (the Journal command's own Run after the DB is opened)"
- "migrate.sh imports from a copy of the export in which only empty \"translations\": [] arrays become {}; the export directory stays untouched and the importer fix is deferred to sm-journal-plugin"
- "The URL rehearsal gives each URL its own X-Forwarded-For visitor (203.0.113.N): one address for ~90 SSR pages exceeds Go's 120/min per-visitor journal limit, and a broken XFF chain would still surface as 429s"
- "The nginx image location declares its own types map (jpeg/png/gif/webp, octet-stream default) so images keep an image type under nosniff whatever the server's mime.types; the regex is case-insensitive (~*) for upper-case extensions"
- "The boot test loads a temp copy of config/ so a developer's gitignored .env cannot override backend.uri"
patterns-established:
- "scripts/smoke.sh --url BASE as the deployed-site check over urls.txt (16-04 reuses it on staging)"
requirements-completed: [SC-1, SC-3]
coverage:
- id: D1
description: "Admin at /gfadmin, no /backend route; the host journal gitlink points at 3d533d4 with the D-20 and D-21 additions (D-09)"
requirement: SC-1
verification:
- kind: unit
ref: "go -C ../sm-grzybyfunkcjonalne-app test ./... -run '^(TestBootUserTranslateJournal|TestCORS)$' (assertAdminMount, assertGrzybyCommands)"
status: pass
- kind: integration
ref: "scripts/smoke.sh --go-only: GET /gfadmin non-404, GET /backend 404"
status: pass
human_judgment: false
- id: D2
description: "grzyby:import-featured-images copies the original under attach.BlobKey and attaches it to the post; the post answers on its pl slug with featured_image_url /storage/app/uploads/public/665/f1e/2a3/665f1e2a3b4c5d6e7f8091.png; a second run creates nothing (D-22)"
requirement: SC-1
verification:
- kind: integration
ref: "scripts/smoke.sh --go-only -> smoke: ok (go) (featured_image_url, file on disk equals source, base slug 404, 'Results: 0 created, 1 skipped, 0 failed')"
status: pass
human_judgment: false
- id: D3
description: "grzyby:import-categories fills name, description and content preferring pl; cordyceps carries the content marker, amanita-muscaria is listed with post_count 0; a rerun reports 0 updated, 1 unchanged (D-21, D-24)"
requirement: SC-3
verification:
- kind: integration
ref: "scripts/smoke.sh --go-only (categories?include_empty=1 checks and the second run)"
status: pass
- kind: integration
ref: "scripts/php-export/categories.sql and featured-images.sql run on a throwaway MariaDB 12.3 fixture; output parsed by the Go command (--dry-run)"
status: pass
human_judgment: false
- id: D4
description: "scripts/migrate.sh runs migrate, the fresh-database guard, the asserted pl default, the one-shot import, categories, media, featured images, author and the gap report in that order (D-25, D-17, D-15)"
requirement: SC-1
verification:
- kind: integration
ref: "scripts/smoke.sh (full) runs migrate, admin:create and migrate.sh --author-login admin --author-slug admin; 63 published posts served (meta.total)"
status: pass
- kind: manual_procedural
ref: "migrate.sh --dry-run and the unknown-author path run by hand on a scratch database (dies with 'author login ... does not exist')"
status: pass
human_judgment: false
- id: D5
description: "Staging deploy files: nginx vhost passes nginx -t; both supervisor programs parse with the required keys; /backend 404, image regex with nosniff, plain /storage/ 404, GET/HEAD only to Nuxt (D-07, D-10)"
ref: "the vhost run under a non-root nginx 1.30.4 on loopback: images 200 image/png with nosniff and HSTS, x.php and config.yaml 404, /backend 404, POST / 403, HTTP 301 to HTTPS"
status: pass
human_judgment: false
- id: D6
description: "Local rehearsal answers every non-remote urls.txt line: all 71 live sitemap URLs, /blog?page=2 keeping ?page=2, /amanita-muscaria 200, /autor/admin 200, /backend 404, /en/blog 404, /rss.xml as RSS (D-15, D-18)"
requirement: SC-3
verification:
- kind: e2e
ref: "scripts/build.sh && scripts/smoke.sh -> smoke: ok (95 urls)"
status: pass
human_judgment: false
- id: D7
description: "The runbook and deploy files work on the real PHP server (table names, morphs, Postgres and Node versions, paths)"
verification: []
human_judgment: true
rationale: "Only the staging deploy in 16-04 can confirm the server-side values the SQL files and deploy files assume"
duration: 22 min
completed: 2026-10-09
---
# Phase 16 Plan 03: Host, migration runbook and staging deploy Summary
**`sm-grzybyfunkcjonalne-app` is now the deployable host.**
- The admin is at `/gfadmin`.
- An app plugin `golem15.grzyby` imports featured images under their Winter disk names and the category pillar content from JSON Lines maps produced by SQL on the PHP database.
- One ordered runbook does the migration: migrate, an asserted pl default, a one-shot journal import, categories, media, featured images, the author and a gap report.
- Staging deploy files cover two processes (Go on 8096, Nuxt on 3016 behind one nginx vhost).
- A local rehearsal on a throwaway database answers all 95 local URL checks, including every one of the 71 live sitemap URLs.
## Performance
- **Duration:** 22 min
- **Started:** 2026-10-09T16:31:36Z
- **Completed:** 2026-10-09T16:54:03Z
- **Tasks:** 3 (Task 1 a tracer, auto-verified end to end before expansion)
- **Files:** 32 changed in the host (2,701 insertions), plus 1 todo and `deferred-items.md` in summercms.go
## Accomplishments
- **D-09.** `config/backend.yaml` sets `uri: /gfadmin`. The boot test asserts that some route starts with `/gfadmin/` and that none is under `/backend`. The nginx vhost answers 404 for `/backend`.
- **Journal gitlink.** It now points at `3d533d4`, which carries the D-20 author endpoints and the D-21 category content.
- The nested clone had working-tree drift. I saved it as a patch and checked it against the upstream commits; all of it was already contained in `be4f33a..3d533d4`.
- I then discarded the drift file by file and checked out `3d533d4`.
- **`golem15.grzyby`.** It was scaffolded with `summer make:plugin` and registered with `summer plugin:add`. `summer build` regenerated `plugins.gen.go` (`main.go` was unchanged). I stripped the plugin down to commands only:
-`grzyby:import-featured-images` (D-22, T-16-12): validates the disk name, applies the content-type allowlist and sniffs the bytes, keeps the source path inside `--source` and checks the size. It falls back from the base slug to the pl slug through the export, writes the blob under `attach.BlobKey` and inserts the `system_files` row. It is idempotent.
-`grzyby:import-categories` (D-21): prefers pl values and finds the category by pl slug, then base slug. It accepts `translations.pl` as an object, a JSON string (as MariaDB nests it) or null. Equal values count as unchanged.
-`grzyby:import-posts`: see deviation 1.
- **The runbook (`scripts/migrate.sh`).** Steps run in the D-25 order.
- It refuses a database that already holds posts (Pitfall 3).
- It asserts that `pl` is the only enabled default before importing (Pitfall 1).
- The author step sets `golem15_bloghub_author_slug` and assigns every post without an author.
- The gap report flags and does not patch (D-17).
-`--dry-run` runs only the steps that write nothing.
- **PHP exports.**
-`featured-images.sql` and `categories.sql` emit one `JSON_OBJECT` per row. I ran both against a throwaway MariaDB 12.3 with a fixture schema, and the Go command parsed the output.
- The README names the server checks: the post and category morphs and tables, and the `winter_`/`rainlab_`/`golem15_` translate table prefixes. It also gives the `sed` rewrites for the Blog plugin and the copy steps.
- **Deploy.**
- nginx vhost with:
-`/gfadmin` and `/_journal/` proxied to Go (64 MiB body limit);
- the image regex location with nosniff, an explicit types map and 30-day expiry;
- a plain `/storage/` prefix answering 404;
-`limit_except GET HEAD` for Nuxt;
-`X-Forwarded-For` on every proxy.
- Supervisor programs `grzybyfunkcjonalne-go` and `grzybyfunkcjonalne-nuxt` (Node `--env-file`).
-`env.example` (adds `SUMMER_URL`) and `nuxt.env.example` (`NUXT_SITE_INDEXABLE=false`, empty GA).
- **Build and checks.**
-`scripts/build.sh` builds `bin/grzybyfunkcjonalne` (linux/amd64) and `build/nuxt/.output`, and refuses generated-source drift.
-`scripts/check-deploy.sh` runs nginx -t in a temp prefix, checks both supervisor programs with configparser, and greps the vhost for its security locations.
- **`scripts/urls.txt`.**
- It holds the **71 live sitemap URLs** fetched 2026-10-09 (`/`, `/blog`, 7 categories, 62 posts), the same set as at research time.
- It adds 27 more lines: pagination, static pages, 404 cases, Go paths and 3 remote nginx lines, for 98 checks in total.
-`scripts/smoke.sh` checks all 95 non-remote lines locally and every line with `--url`.
## Task Commits
All code commits are in `../sm-grzybyfunkcjonalne-app` (`master`, pushed `33bc5e0..8f7f2de` after sm-journal-plugin reached origin):
0.`90f2dbd` chore: point journal gitlink at author endpoint and category content
1.**Task 1 (tracer):**
-`5ccbc62` feat: mount the admin at /gfadmin
-`893cf42` feat: app plugin golem15.grzyby with featured-image import
-`a3266c3` fix: open the database for grzyby commands and add grzyby:import-posts
-`3f17f1c` feat: migration runbook and Go rehearsal
2.**Task 2:**
-`30233d5` feat: grzyby:import-categories for category content
-`81ffce3` feat: runbook categories, media and author steps; PHP export queries
-`6ff7d5d` (summercms.go) docs: todo for Journal HTML content re-save
3.**Task 3:**
-`07390b2` feat: staging deploy files and full URL rehearsal
-`8f7f2de` docs: host README
**Tracer gate.** Interactive mode with `end-of-phase` and an automated-only `<verify>`. The verify was re-run and passed (vet, boot test, plugin vet, build, `smoke: ok (go)`, no `uri: /backend`) before expansion.
**Plan metadata:** the docs commit in summercms.go (this SUMMARY, `deferred-items.md`, and my STATE/ROADMAP lines only).
## Files Created/Modified
See `key-files` in the frontmatter. Also:
-`plugins/grzyby/go.mod`/`go.sum` (local replaces for the journal, translate and user modules).
- The host `go.mod` and `go.work` (written by `summer plugin:add`).
- The testdata 1×1 PNG at `plugins/grzyby/testdata/featured/uploads/665/f1e/2a3/665f1e2a3b4c5d6e7f8091.png` (69 bytes).
## Decisions Made
See `key-decisions` in the frontmatter.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] Plugin commands get no database in a `summer build` host**
- **Found during:** Task 1 (first rehearsal run: `journal: database is not configured`).
- **Issue:** The generated `main.go` publishes no `*gorm.DB` for plugin commands. Only lagoon, cabana and `serve` open one. So `journal:import-posts` cannot run from the host CLI at all.
- **Fix:**
-`lookupDB` opens and publishes `database.dsn` through `lagoon.OpenFromApp` and `lagoon.Publish`, which also runs the `OnDatabase` hooks.
- A thin `grzyby:import-posts` runs the Journal command's own `Run`, with the same flags, after that.
-`migrate.sh` calls it, and its step names say `journal:import-posts`.
- The framework fix is in `deferred-items.md`. This is outside this plan's files and the concurrency boundary (`modules/`).
- **Found during:** Task 1 (61 of 63 published posts served).
- **Issue:** The PHP exporter writes empty translations as `[]`. Five files failed as "Invalid JSON", including the live post `ekstrakt-z-turkey-tail`.
- **Fix:** `migrate.sh` imports from a temp copy in which only that empty array becomes `{}`, and prints the count. The importer fix is deferred to sm-journal-plugin.
- **Commit:** `3f17f1c`.
**3. [Rule 3 - Blocking] A local `.env` overrode `backend.uri` in the boot test**
- **Issue:** compass reads `../.env` next to `config/`. The developer's gitignored host `.env`, which I did not read, keeps the admin at `/backend`, so the new `/gfadmin` assertion failed locally even though a clean work dir mounts `/gfadmin`.
- **Fix:** The boot test loads a temp copy of `config/`.
- **Commit:** `893cf42`.
**4. [Rule 1 - Plan data] `/sitemap.xml` content type**
- **Issue:** `@nuxtjs/sitemap` serves `text/xml; charset=UTF-8`, not `application/xml`.
- **Fix:** `urls.txt` expects `type=text/xml`.
- **Commit:** `07390b2`.
**5. [Rule 1 - Test design] One visitor address exhausted the Go rate limit**
- **Issue:** With `X-Forwarded-For: 203.0.113.7` on every URL, 28 checks got 429. About 90 SSR pages make more than 120 API calls a minute for that one visitor. This also proved the XFF chain works: the bucket used was the visitor's, not Nuxt's.
- **Fix:** Each URL uses its own `203.0.113.N`.
- **Commit:** `07390b2`.
**6. [Rule 2 - Missing critical] Image types under nosniff**
- **Issue:** Under a bare `http {}`, as in check-deploy, nginx served the images as `text/plain` with nosniff.
- **Fix:** The image location declares its own `types` map and an octet-stream default, and matches case-insensitively (`~*`) for files like `.PNG`.
- **Verification:** On loopback, the images answer `200 image/png`.
- **Fix:** `http.DetectContentType` must match the declared type, and the file size must match the map.
- **Commit:** `893cf42`.
**8. [Scope adjustment] The UAT-host gap report**
- **Issue:** The 2 known `gf.uat.golem15.com` references are only in the base text of one export file. The pl import replaces that text, so no imported body carries them.
- **Fix:** The report lists the export file and the imported count (0) instead of claiming 2 broken bodies.
- **Commit:** `81ffce3`.
**9. [Scope adjustment] The scaffold was trimmed**
- **Issue:** `make:plugin` writes the full scaffold (config, lang, mail views, `registry.gen.go`, `routes.go`).
- **Fix:** I removed it. The plugin is `plugin.go` plus `console/`, with no routes, so D-14 is unchanged. `registry.gen.go` is refreshed only by `make:*` artifact commands, not by `summer build`.
- **Commit:** `893cf42`.
---
**Total deviations:** 9. Three were blocking fixes, two were missing-critical additions, two were plan-data or test-design corrections, and two were scope adjustments.
**Impact on plan:** Every planned artifact, command name, flag and key link exists. The only additions are `grzyby:import-posts` and the export normalization, both workarounds for upstream gaps that are now recorded in `deferred-items.md`.
## Issues Encountered
- **Precondition.** `SMOKE_ADMIN_DSN` was unset and Docker was unreachable for user `golem`.
- I used the orchestrator's sanctioned fallback: a private PostgreSQL 18.6 cluster (`initdb`, ICU, trust auth) on 127.0.0.1:55494 in the scratchpad.
-`SMOKE_ADMIN_DSN` pointed at that cluster for every rehearsal.
- The cluster and a throwaway MariaDB were stopped and removed at the end.
- No temporary override touched any repo file.
- **pnpm.** pnpm ran with `TMPDIR` set to the scratchpad and the project-drive store (`npm_config_store_dir`), as in 16-01 and 16-02.
- **`@nuxt/fonts`.** Its metadata fetches are blocked in the sandbox. The builds succeed with warnings only.
- **Pre-existing state.** The host's untracked `.gsd/` was left alone.
- **Concurrent edits.** Another session has uncommitted STATE/ROADMAP edits (Phase 18.1). This plan's metadata commit stages only its own changes and leaves theirs in the working tree.
## Known Stubs
None.
## Threat Flags
None beyond the plan's threat model. T-16-10 through T-16-17 and T-16-SC are implemented as planned:
- no new Go HTTP endpoint;
- no new Go or npm module (the plugin uses gorm, gocloud.dev, the framework and sm-journal-plugin, all already in the graph);
- templates carry empty secrets, which the negative grep confirms;
- the smoke drops only its `grzyby_smoke_<hex>` database.
## User Setup Required
None for this plan. `SMOKE_ADMIN_DSN` is optional (Docker or the user's own Postgres). The server values are confirmed in 16-04.
## Next Phase Readiness
**16-04 (staging deploy).**
- On the server:
1. Run the two SQL files after the checks in `scripts/php-export/README.md`.
2. Copy the uploads, media and export.
3. Follow the README runbook (migrate, `admin:create`, `migrate.sh`).
4. Install the deploy files.
5. Run `scripts/smoke.sh --url https://staging.grzybyfunkcjonalne.pl`.
- Decide on `first-journal-post` (published in the export, absent live).
- Swap `listen ... http2` for `http2 on;` if the server's nginx is 1.25.1 or newer.
**16-05 (unit tests).** These can target:
-`ReadFeaturedImageMap` and `ImportFeaturedImages` (mem:// bucket, idempotency, path escape, type allowlist and sniffing);
-`ReadCategoryMap`, `CategoryTranslations.UnmarshalJSON` and `ImportCategories` (unchanged detection);
-`validDiskName`, `flagOrDefault` and `flagOn`.
## Self-Check: PASSED
- FOUND: every `key-files.created` path in the host and the todo in summercms.go.
- FOUND: host commits `90f2dbd 5ccbc62 893cf42 a3266c3 3f17f1c 30233d5 81ffce3 07390b2 8f7f2de` on `origin/master`, and summercms.go `6ff7d5d`.
- Plan verification, re-run after the last commit:
-`check-deploy: ok`;
-`build: bin/grzybyfunkcjonalne and build/nuxt/.output ready`;
-`smoke: ok (95 urls)`;
-`go vet ./...` and `go test ./... -count=1` ok;
- the env.example negative grep passes;
- every one of the 71 live locs has its `200 PATH` line in `urls.txt`.
- **vue-journal-layer: visitor address lost under Nitro swr route caching.** `routeRules` swr renders a page from a copy of the request that keeps only `varies` headers, so the layer's `useJournalApi` reads no `x-forwarded-for` and forwards only the Nuxt peer. Every SSR cache miss would then share one Go rate-limit bucket (RESEARCH Pitfall 5). The site app works around it with `server/plugins/client-address.ts` (records the address in `event.context` on the Nitro `request` hook) and an app-level `useJournalApi` override. Fix it in the layer (a layer Nitro plugin plus the composable reading `event.context`), release a new tag, bump the app and delete the app override. The layer's `/rss.xml` and sitemap source have the same gap on their cache misses (once per TTL).
- **vue-journal-layer smoke tool sends no Accept header.** Undici's `fetch` sends `sec-fetch-mode: cors`, so Nuxt answers errors with JSON and an expectation such as `404 /404 contains=...` cannot see the error page. The app works around it with `server/plugins/html-errors.ts`, which treats page paths as HTML requests. An `Accept: text/html` default in `testing/smoke-ssr.mjs` would let other apps check error copy without such a plugin.
- **`public/images/cta.png` is 5.5 MB** (1792×1024 PNG, kept byte for byte from the live site as the plan required). It is the CTA band and blog hero background. A WebP derivative (the hero photo is 0.47 MB as WebP) would cut page weight; decide before the production cutover.
## From 16-03 (sm-grzybyfunkcjonalne-app host, runbook and deploy)
- **summercms.go: the generated main.go publishes no database for plugin commands.** `lagoon`, `cabana` and `surf serve` open the database themselves (`lagoon.OpenFromApp`), but a plugin's `pact.HasCommands` command finds no `*gorm.DB` on the app. So `journal:import-posts`, `journal:export-posts` and `user:token:*` stop with "database is not configured" in every `summer build` host (fonoteka.go's hand-written `app.go` publishes it). The host works around it with `grzyby:import-posts` and a `lookupDB` helper that open and publish the database through lagoon. Fix it in the framework, for example as a lazy publish before plugin command `Run`, so the wrapper can go.
- **sm-journal-plugin: `journal:import-posts` rejects `"translations": []`.** The PHP exporter writes a post without translations as `[]` (json_encode of an empty PHP array), and the Go `PostFile.Translations map[string]map[string]any` fails to unmarshal it ("Invalid JSON"). This affects 5 of the 105 grzybyfunkcjonalne export files; 2 of them are published, including the live post `ekstrakt-z-turkey-tail`. `scripts/migrate.sh` imports from a copy in which only that empty array becomes `{}`. The importer should accept an empty array as an empty map (PHP parity).
- **`first-journal-post` is published in the export but not on the live site.** "First journal post" was published 2026-02-23. It is absent from the live sitemap, so Go serves 63 published posts where the live site lists 62. Unpublish or delete it after the staging import (16-04), or confirm it should stay.
- **The UAT-host image references are not in the imported bodies.** The 2 `https://gf.uat.golem15.com/storage/...` references sit only in the base (older) content of `wprowadzenie-do-grzyba-cordyceps`. With pl as the default, the pl body replaces the base text, so the gap report shows 1 export file and 0 imported bodies. Nothing is broken on the new stack. The report keeps listing the export file.
- **nginx `listen ... http2` is deprecated on nginx 1.25.1 and newer** (a warning, not an error). The staging vhost keeps the summercms.io form because the PHP server's nginx version is unknown. Switch to `http2 on;` once 16-04 confirms the version.
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.