diff --git a/.planning/phases/16.2-plugin-provided-field-types-and-the-blog-editor-formwidget/16.2-04-SUMMARY.md b/.planning/phases/16.2-plugin-provided-field-types-and-the-blog-editor-formwidget/16.2-04-SUMMARY.md new file mode 100644 index 0000000..4294c90 --- /dev/null +++ b/.planning/phases/16.2-plugin-provided-field-types-and-the-blog-editor-formwidget/16.2-04-SUMMARY.md @@ -0,0 +1,247 @@ +--- +phase: 16.2-plugin-provided-field-types-and-the-blog-editor-formwidget +plan: 04 +subsystem: journal-frontend +tags: [vue-journal-layer, nuxt, sanitize-html, embeds, youtube-nocookie, release, pnpm, vue-grzybyfunkcjonalne-app] + +requires: + - phase: 16.2-02 + provides: tests/blogeditor/fixtures/editor-corpus.json (the Blog Editor's output corpus) and the placeholder format
+ - phase: 16.1 + provides: sanitize-html 2.18.0 mirror (htmlGate.ts, decision note 0001-sanitize-html.md), layer tag v0.1.1, app pin #v0.1.1 +provides: + - "vue-journal-layer htmlGate.ts mirror of the Go editor-output extension: figure data-embed ^youtube$, figure data-id ^[A-Za-z0-9_-]{11}$, img alt free text, ol start INTEGER" + - "vue-journal-layer app/utils/embeds.ts: EMBED_PLACEHOLDER, EMBED_PROVIDERS (youtube only), expandEmbeds(html)" + - "JournalPostBody and JournalCategoryHeader render expandEmbeds(sanitizeJournalHtml(html)); expansion is last, payload transforms only sanitize" + - "CSS classes .journal-embed and .journal-embed--youtube (16 / 9 frame)" + - "tests/fixtures/editor-corpus.json, a byte copy of plan 02's corpus" + - "Annotated tag vue-journal-layer v0.2.0 (tag object 042b65d0, commit 759e684e) on origin, installable anonymously over git+https" + - "vue-grzybyfunkcjonalne-app main 465286a pins #v0.2.0 with a lockfile resolving to 759e684e" +affects: [16.2-05, 16.2-06, vue-journal-layer, vue-grzybyfunkcjonalne-app] + +actuals: + tokens: 9700 + tasks: 3 + commits: 3 +plan_head_before: 294a8d8b19b999d54cc3b1266d0f42968159f5f2 +plan_head_after: 759e684e862bd8a2f34e2bbcdf258f66aeddad4f +commits_repo: ../vue-journal-layer +app_head_before: ada03b202bdfce2c09812dc81c58b0890cf4b86b +app_head_after: 465286a880f0aae6118c2915aa1475a5da353295 +app_commits_repo: ../sm-grzybyfunkcjonalne-app/vue-grzybyfunkcjonalne-app +app_commits: 1 + +tech-stack: + added: [] + patterns: + - "Sanitize first, expand last: the v-html sink runs sanitizeJournalHtml and then expandEmbeds; nothing sanitizes after expansion, and payload transforms never expand" + - "Exact-markup placeholder expansion: one global RegExp matching the sanitizer's serialization, a provider allowlist with an id pattern, and an iframe src built from a fixed host plus the validated id; any other match is returned unchanged" + - "Mirror pinned by the editor's corpus: the layer keeps a byte copy of the Go side's corpus and every entry must be safe under gateJournalHtml" + +key-files: + created: + - ../vue-journal-layer/app/utils/embeds.ts + - ../vue-journal-layer/tests/embeds.test.ts + - ../vue-journal-layer/tests/fixtures/editor-corpus.json + modified: + - ../vue-journal-layer/app/utils/htmlGate.ts + - ../vue-journal-layer/app/components/Journal/PostBody.vue + - ../vue-journal-layer/app/components/Journal/CategoryHeader.vue + - ../vue-journal-layer/app/assets/css/journal-base.css + - ../vue-journal-layer/tests/htmlGate.test.ts + - ../vue-journal-layer/testing/fixtures/acme/posts.json + - ../vue-journal-layer/testing/expect-acme.txt + - ../vue-journal-layer/docs/decisions/0001-sanitize-html.md + - ../vue-journal-layer/README.md + - ../vue-journal-layer/package.json + - ../sm-grzybyfunkcjonalne-app/vue-grzybyfunkcjonalne-app/package.json + - ../sm-grzybyfunkcjonalne-app/vue-grzybyfunkcjonalne-app/pnpm-lock.yaml + +key-decisions: + - "The layer mirror gained exactly the four editor-output rules of the Go policy (figure data-embed ^youtube$ and data-id ^[A-Za-z0-9_-]{11}$, img alt free text, ol start INTEGER); no fifth rule was needed for the corpus" + - "Embed expansion runs only at the v-html sinks, after sanitizeJournalHtml; the payload (toPostView, CategoryPage) carries the placeholder, never the iframe" + - "Human approved the one-way release at the Task 2 blocking-human checkpoint with the reply \"push\"; vue-journal-layer v0.2.0 is published and any later defect gets v0.2.1" + - "The release commit also moved the README install line from #v0.1.0 to #v0.2.0 so the documented pin matches the published tag" + +patterns-established: + - "A layer release is a separate 'chore: release vX.Y.Z' commit (package.json version plus README install line), tagged annotated, pushed after a blocking-human checkpoint, proven by scripts/check-git-install.sh, then pinned in the consuming app with a regenerated lockfile" + +requirements-completed: [D-05, D-17] + +coverage: + - id: D1 + description: "htmlGate.ts mirrors the Go editor-output extension and nothing more; every corpus entry is safe, the placeholder survives two sanitizer passes, negative cases (other provider, 10/12-char id, data-id on div, other data attributes, iframe, ol start=x) are stripped" + requirement: "D-05" + verification: + - kind: unit + ref: "../vue-journal-layer/tests/htmlGate.test.ts (npm test: 136 pass, 0 fail)" + status: pass + - kind: other + ref: "cmp ../vue-journal-layer/tests/fixtures/editor-corpus.json ../sm-journal-plugin/tests/blogeditor/fixtures/editor-corpus.json (exit 0)" + status: pass + human_judgment: false + - id: D2 + description: "expandEmbeds turns only the exact allowlisted placeholder into a youtube-nocookie iframe, last at the post and category sinks; a raw stored iframe is stripped before expansion" + requirement: "D-17" + verification: + - kind: unit + ref: "../vue-journal-layer/tests/embeds.test.ts (npm test: 136 pass, 0 fail)" + status: pass + - kind: integration + ref: "node testing/smoke-ssr.mjs against the built playground: smoke-ssr: ok (29 checks), including 200 /blog/hello-acme contains=https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ" + status: pass + human_judgment: false + - id: D3 + description: "vue-journal-layer v0.2.0 is an annotated tag on origin and installs anonymously; the scratch lockfile pins the tag commit and the acme SSR expectations pass" + requirement: "D-17" + verification: + - kind: integration + ref: "bash ../vue-journal-layer/scripts/check-git-install.sh v0.2.0 -> check-git-install: ok v0.2.0 759e684e862bd8a2f34e2bbcdf258f66aeddad4f" + status: pass + human_judgment: false + - id: D4 + description: "vue-grzybyfunkcjonalne-app main pins #v0.2.0, its lockfile resolves to 759e684e, it builds, its unit and SSR tests pass, and main equals origin/main" + requirement: "D-17" + verification: + - kind: integration + ref: "Task 3 app verify command -> # pass 26, # fail 0, app-pin-ok; pnpm run smoke:ssr -> smoke-ssr: ok (37 checks)" + status: pass + human_judgment: false + - id: D5 + description: "A Blog Editor YouTube embed plays on the deployed grzybyfunkcjonalne.pl UAT page" + requirement: "D-17" + verification: [] + human_judgment: true + rationale: "The UAT deploy and the CSP frame-src https://www.youtube-nocookie.com addition belong to plan 05; a real browser on the deployed site is needed to confirm the frame loads" + +duration: "about 50 min across two executor runs (Task 3 continuation 7 min)" +completed: 2026-10-10 +status: complete +--- + +# Phase 16.2 Plan 04: Journal Embeds in the Nuxt Layer Summary + +**vue-journal-layer v0.2.0 keeps the Blog Editor's markup through its sanitize-html mirror (four rules matching the Go policy) and expands `
` placeholders into youtube-nocookie iframes as the last step before v-html. The tag is published and installs anonymously, and vue-grzybyfunkcjonalne-app main pins it.** + +## Performance + +- **Duration:** about 50 min over two executor runs. The first run covered Task 1, the release commit and the Task 2 checkpoint (layer commits at 10:19Z). This continuation ran Task 3 from 2026-10-10T11:01:53Z to about 11:08Z. +- **Started:** about 2026-10-10T10:14Z, after plan 03 closed. The prior run did not record an exact start time. +- **Completed:** 2026-10-10T11:10Z +- **Tasks:** 3/3 (Task 1 tracer, Task 2 blocking-human decision, Task 3 release) +- **Files modified:** 15 (13 in vue-journal-layer, 2 in vue-grzybyfunkcjonalne-app) + +## Accomplishments + +- **Mirror (D-05 amended).** `htmlGate.ts` gained exactly the four editor-output rules that plan 03 added to the Go bluemonday policy: figure `data-embed` `^youtube$`, figure `data-id` `^[A-Za-z0-9_-]{11}$`, img `alt` free text and ol `start` INTEGER. Every entry of the byte-copied corpus is safe. The Policy section of decision note 0001 lists the four additions. +- **Embeds (D-17).** `app/utils/embeds.ts` exports `EMBED_PLACEHOLDER`, `EMBED_PROVIDERS` (YouTube only) and `expandEmbeds`. The iframe src is the fixed host `https://www.youtube-nocookie.com/embed/` plus the validated id, and the iframe gets lazy loading, a strict-origin-when-cross-origin referrer policy and a restricted `allow` list. +- **Order.** `JournalPostBody` and `JournalCategoryHeader` render `expandEmbeds(sanitizeJournalHtml(html))`. `toPostView` and `CategoryPage.vue` only sanitize. A raw iframe in stored HTML is stripped before expansion. +- **SSR proof.** The acme fixture post `hello-acme` carries a placeholder, and the built playground renders the iframe for it. +- **Release.** Annotated tag `v0.2.0` is on origin and `check-git-install.sh v0.2.0` passes. +- **App pin.** The site app pins `#v0.2.0` on its pushed main, with a lockfile that resolves to the tag commit. + +## Task Commits + +| Task | Name | Repo | Commit | +|---|---|---|---| +| 1 | Tracer: a stored placeholder renders as a privacy-enhanced YouTube iframe | vue-journal-layer | `a6ee443` (feat) | +| 2 | Approve pushing vue-journal-layer v0.2.0 and the site app pin (prep: release commit) | vue-journal-layer | `759e684` (chore: release v0.2.0) | +| 3 | Push the layer tag, prove the git install, pin v0.2.0 in the site app | vue-grzybyfunkcjonalne-app | `465286a` (chore(deps)) | + +Task 3 also created the annotated tag `v0.2.0` (tag object `042b65d072f276f7ee6f1d8bc6f66f518411161e`, peeled commit `759e684e862bd8a2f34e2bbcdf258f66aeddad4f`). + +**Pushed heads:** +- vue-journal-layer `main`: `294a8d8..759e684` (`759e684e862bd8a2f34e2bbcdf258f66aeddad4f`) and tag `v0.2.0`, pushed to git@git.golem15.com:golem15/vue-journal-layer.git. +- vue-grzybyfunkcjonalne-app `main`: `ada03b2..465286a` (`465286a880f0aae6118c2915aa1475a5da353295`), pushed to git@git.golem15.com:golem15/vue-grzybyfunkcjonalne-app.git. + +The summercms.go host gitlink and the sm-grzybyfunkcjonalne-app host gitlink were not touched. Plan 05 bumps them. + +## Evidence + +**Task 1 (tracer):** +- `npm test`: 136 pass, 0 fail. +- `pnpm install --frozen-lockfile` and `build:playground`: OK. +- `smoke-ssr: ok (29 checks)`, including `200 /blog/hello-acme contains=https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ`. +- `cmp tests/fixtures/editor-corpus.json ../sm-journal-plugin/tests/blogeditor/fixtures/editor-corpus.json`: exit 0. +- Exactly four mirror rules were added: figure data-embed `^youtube$`, figure data-id `^[A-Za-z0-9_-]{11}$`, img alt FREE_TEXT, ol start INTEGER. + +**Task 2 (checkpoint:decision, gate blocking-human):** +- The verify command printed `push-precheck-ok`. +- Release sheet: + - Layer commits to push: `a6ee443` feat, `759e684` chore: release v0.2.0. + - Tag `v0.2.0`, message "v0.2.0: Blog Editor markup kept by the sanitizer; Journal embed placeholders expand into privacy-enhanced YouTube iframes". + - App change: pin `#v0.1.1` to `#v0.2.0`, plus the regenerated lockfile. + - Rollback: a defect means v0.2.1, and the app pin can go back to `#v0.1.1`. +- Human reply, verbatim: "push" + +**Task 3 (release):** +- Re-check before tagging: `git ls-remote --tags origin refs/tags/v0.2.0` was empty, the layer and app trees were clean, and neither was behind origin/main. +- `git ls-remote --tags origin`: `042b65d0… refs/tags/v0.2.0` and `759e684e… refs/tags/v0.2.0^{}`. +- `bash scripts/check-git-install.sh v0.2.0`: `smoke-ssr: ok (29 checks)`, then `check-git-install: ok v0.2.0 759e684e862bd8a2f34e2bbcdf258f66aeddad4f`. +- App `pnpm install`: `- vue-journal-layer 0.1.0` / `+ vue-journal-layer 0.2.0`. The lockfile diff only touches the layer's specifier, version and resolution `commit: 759e684e862bd8a2f34e2bbcdf258f66aeddad4f`. +- App verify (`grep pin && pnpm install --frozen-lockfile && pnpm build && pnpm test && HEAD == origin/main`): `# pass 26`, `# fail 0`, `app-pin-ok`. +- App `pnpm run smoke:ssr` (extra check): `smoke-ssr: ok (37 checks)`. + +## Files Created/Modified + +- `../vue-journal-layer/app/utils/embeds.ts`: placeholder RegExp, provider allowlist and `expandEmbeds`. +- `../vue-journal-layer/app/utils/htmlGate.ts`: figure and ol entries and the widened img alt rule. +- `../vue-journal-layer/app/components/Journal/PostBody.vue` and `CategoryHeader.vue`: sanitize, then expand, then v-html. +- `../vue-journal-layer/app/assets/css/journal-base.css`: `.journal-embed` frame at a 16 / 9 aspect ratio. +- `../vue-journal-layer/tests/embeds.test.ts`, `tests/htmlGate.test.ts` and `tests/fixtures/editor-corpus.json`: transform, pipeline and mirror tests, plus the corpus copy. +- `../vue-journal-layer/testing/fixtures/acme/posts.json` and `testing/expect-acme.txt`: the SSR fixture and its expectation. +- `../vue-journal-layer/docs/decisions/0001-sanitize-html.md` and `README.md`: the Policy additions and the Embeds section. +- `../vue-journal-layer/package.json`: version 0.2.0. +- `../sm-grzybyfunkcjonalne-app/vue-grzybyfunkcjonalne-app/package.json` and `pnpm-lock.yaml`: the `#v0.2.0` pin, resolving to 759e684e. + +## Decisions Made + +- The mirror has exactly four rules, matching the Go policy. The corpus needed no fifth rule. +- Expansion happens only at the sinks and comes last, so the payload carries the placeholder and never an iframe. +- The release is a separate commit, and the README install line moves with the version. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 2 - Correctness] README install line bumped with the version** +- **Found during:** Task 2 (release prep, prior run) +- **Issue:** The plan's release commit changes only the package.json version, but the README install snippet still pinned `#v0.1.0`. A consumer copying it would install an outdated layer. +- **Fix:** The release commit also changes the README install line from `#v0.1.0` to `#v0.2.0`. +- **Files modified:** ../vue-journal-layer/README.md +- **Committed in:** `759e684` (chore: release v0.2.0) + +**2. [Rule 3 - Blocking] check-git-install could not write the shared /tmp pnpm store** +- **Found during:** Task 3 +- **Issue:** The script's scratch app lives under `$TMPDIR` (/tmp), where pnpm picks the store `/tmp/.pnpm-store/v11`. On this machine another local user (`jin`) owns that store, so `pnpm install` failed with `ERR_SQLITE_ERROR attempt to write a readonly database`. The tag was not the cause. +- **Fix:** I re-ran the unchanged script with `TMPDIR` and `npm_config_store_dir`/`pnpm_config_store_dir` pointing into the session scratchpad. The script, the tag and the remote were not touched. +- **Files modified:** none +- **Verification:** `check-git-install: ok v0.2.0 759e684e862bd8a2f34e2bbcdf258f66aeddad4f` + +--- + +**Total deviations:** 2 (1 correctness, 1 environment blocker). +**Impact on plan:** No change to the shipped code. The tag was created and pushed exactly as planned, with no moved tag and no force-push. + +## Issues Encountered + +- On this machine, `scripts/check-git-install.sh` fails under the default `/tmp` because the store there belongs to another user (see deviation 2). This is a machine-local environment issue, not a layer defect. Anyone re-running the script here should set `npm_config_store_dir`, or a `TMPDIR` whose pnpm store they own. + +## User Setup Required + +None. No external service configuration is required. + +## Next Phase Readiness + +- Plan 05 can bump the sm-grzybyfunkcjonalne-app host gitlink to app `465286a`. It also needs to add `frame-src https://www.youtube-nocookie.com` to the report-only CSP, then deploy to UAT. +- The plan 06 phase gate can `cmp` the layer corpus copy against `../sm-journal-plugin/tests/blogeditor/fixtures/editor-corpus.json`. + +--- +*Phase: 16.2-plugin-provided-field-types-and-the-blog-editor-formwidget* +*Completed: 2026-10-10* + +## Self-Check: PASSED + +- FOUND: ../vue-journal-layer/app/utils/embeds.ts, tests/embeds.test.ts, tests/fixtures/editor-corpus.json +- FOUND commits: a6ee443, 759e684 (vue-journal-layer), 465286a (vue-grzybyfunkcjonalne-app) +- expandEmbeds count 0 in postView.ts and CategoryPage.vue; app pin #v0.2.0 present; corpus cmp exit 0