docs(quick-261006-sne): markdown preview via sanitized RenderMarkdown
This commit is contained in:
@@ -31,7 +31,7 @@ See: .planning/PROJECT.md (updated 2026-09-16)
|
|||||||
Phase: 15 (Journal plugin) — EXECUTING
|
Phase: 15 (Journal plugin) — EXECUTING
|
||||||
Plan: 4 of 4
|
Plan: 4 of 4
|
||||||
Status: Ready for phase verification
|
Status: Ready for phase verification
|
||||||
Last activity: 2026-10-06 — Completed quick task 261006-seq: Fix nullable pointer scalars rendering as <nil>/address in cabana ML form fields
|
Last activity: 2026-10-06 — Completed quick task 261006-sne: Render Markdown field Preview via sanitized cabana.RenderMarkdown
|
||||||
|
|
||||||
Progress: [██████████] 100%
|
Progress: [██████████] 100%
|
||||||
|
|
||||||
@@ -646,6 +646,7 @@ Recent decisions affecting current work:
|
|||||||
| 261006-eyj | Widget form field payload request and structured data response channel | 2026-10-06 | e723c39 | [261006-eyj-widget-field-payload-and-data-channel](./quick/261006-eyj-widget-field-payload-and-data-channel/) |
|
| 261006-eyj | Widget form field payload request and structured data response channel | 2026-10-06 | e723c39 | [261006-eyj-widget-field-payload-and-data-channel](./quick/261006-eyj-widget-field-payload-and-data-channel/) |
|
||||||
| 261006-s0v | Remove copy-from locale UI from ML text and markdown admin fields | 2026-10-06 | 2f03128 | [261006-s0v-remove-copy-from-locale-ui-from-ml-text-](./quick/261006-s0v-remove-copy-from-locale-ui-from-ml-text-/) |
|
| 261006-s0v | Remove copy-from locale UI from ML text and markdown admin fields | 2026-10-06 | 2f03128 | [261006-s0v-remove-copy-from-locale-ui-from-ml-text-](./quick/261006-s0v-remove-copy-from-locale-ui-from-ml-text-/) |
|
||||||
| 261006-seq | Fix nullable pointer scalars rendering as <nil>/address in cabana ML form fields | 2026-10-06 | 90b87d6 | [261006-seq-fix-nullable-pointer-scalars-rendering-a](./quick/261006-seq-fix-nullable-pointer-scalars-rendering-a/) |
|
| 261006-seq | Fix nullable pointer scalars rendering as <nil>/address in cabana ML form fields | 2026-10-06 | 90b87d6 | [261006-seq-fix-nullable-pointer-scalars-rendering-a](./quick/261006-seq-fix-nullable-pointer-scalars-rendering-a/) |
|
||||||
|
| 261006-sne | Render Markdown field Preview via sanitized cabana.RenderMarkdown | 2026-10-06 | a0116df | [261006-sne-render-markdown-field-preview-via-saniti](./quick/261006-sne-render-markdown-field-preview-via-saniti/) |
|
||||||
|
|
||||||
## Deferred Items
|
## Deferred Items
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
---
|
||||||
|
phase: quick-261006-sne
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- modules/cabana/field_markdown.go
|
||||||
|
- modules/cabana/http.go
|
||||||
|
- modules/cabana/admin_openapi.go
|
||||||
|
- modules/cabana/markdown_preview_test.go
|
||||||
|
- modules/cabana/markdown_preview_route_test.go
|
||||||
|
- modules/cabana/security_coverage_test.go
|
||||||
|
- modules/cabana/phase10_csrf_test.go
|
||||||
|
- modules/cabana/openapi_conformance_test.go
|
||||||
|
- modules/cabana/README.md
|
||||||
|
- admin/openapi/admin.json
|
||||||
|
- admin/src/api/schema.d.ts
|
||||||
|
- admin/src/components/form/fields/MarkdownField.vue
|
||||||
|
- admin/src/styles/main.css
|
||||||
|
- admin/tests/form/MarkdownField.test.ts
|
||||||
|
- admin/tests/form/MLFields.test.ts
|
||||||
|
- docs/backend/forms.md
|
||||||
|
- modules/boardwalk/dist/
|
||||||
|
- scripts/check-phase10.sh
|
||||||
|
- scripts/check-phase12.1.sh
|
||||||
|
- scripts/check-phase12.2.sh
|
||||||
|
- scripts/check-phase14.2.1.sh
|
||||||
|
autonomous: true
|
||||||
|
requirements: [QUICK-261006-sne]
|
||||||
|
|
||||||
|
estimate:
|
||||||
|
tokens: 90000
|
||||||
|
raw_tokens: 90000
|
||||||
|
tasks: 3
|
||||||
|
confidence: low
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Clicking Preview on a type: markdown field shows rendered HTML (`# Title` becomes a heading), not the raw source"
|
||||||
|
- "The preview HTML comes only from the server: the SPA binds data.html of a 2xx answer from POST {prefix}/api/v1/markdown/preview, never the markdown source"
|
||||||
|
- "POST /markdown/preview answers 401 without a backend admin session, 403 for a cookie session without X-Requested-With, and renders through cabana.RenderMarkdown"
|
||||||
|
- "No 200 answer of the preview route contains a script or iframe tag, an event handler, or a javascript/vbscript/data URL; output the RenderMarkdown gate refuses is a 422 validation_failed on `markdown`, shown in the SPA as a text notice"
|
||||||
|
- "On a type: mlmarkdown field the preview renders the active locale's text and re-renders when the locale changes while Preview is open"
|
||||||
|
- "modules/boardwalk/dist matches a fresh build; admin.json and schema.d.ts match a fresh generation; go vet ./... and go test ./... stay green"
|
||||||
|
- "The raw-HTML hygiene gates allow exactly one binding (the sanitized preview line in MarkdownField.vue) and still refuse every other raw-HTML sink"
|
||||||
|
artifacts:
|
||||||
|
- path: modules/cabana/field_markdown.go
|
||||||
|
provides: "service.markdownPreview handler, AdminMarkdownPreviewRequest, AdminMarkdownPreviewResult"
|
||||||
|
- path: modules/cabana/http.go
|
||||||
|
provides: "POST /markdown/preview mounted in the backend-guarded group behind requireAjax"
|
||||||
|
- path: modules/cabana/admin_openapi.go
|
||||||
|
provides: "AdminMarkdownPreview swag annotation"
|
||||||
|
- path: admin/src/components/form/fields/MarkdownField.vue
|
||||||
|
provides: "Preview that fetches server-sanitized HTML (on toggle, debounced on source change) and binds only that answer"
|
||||||
|
- path: modules/boardwalk/dist
|
||||||
|
provides: "Rebuilt embedded admin SPA"
|
||||||
|
key_links:
|
||||||
|
- from: admin/src/components/form/fields/MarkdownField.vue
|
||||||
|
to: modules/cabana/field_markdown.go
|
||||||
|
via: "api.POST('/markdown/preview') typed by admin/src/api/schema.d.ts"
|
||||||
|
pattern: "markdown/preview"
|
||||||
|
- from: modules/cabana/http.go
|
||||||
|
to: modules/cabana/field_markdown.go
|
||||||
|
via: "g.Post(\"/markdown/preview\", requireAjax(s.markdownPreview)) inside the r.GroupRaw(api, []string{\"backend\"}, ...) block"
|
||||||
|
pattern: "markdownPreview"
|
||||||
|
- from: modules/cabana/admin_openapi.go
|
||||||
|
to: admin/openapi/admin.json
|
||||||
|
via: "scripts/check-admin-openapi.sh (swag -> swagger2openapi -> openapi-typescript)"
|
||||||
|
pattern: "/markdown/preview"
|
||||||
|
- from: modules/cabana/security_coverage_test.go
|
||||||
|
to: modules/cabana/http.go
|
||||||
|
via: "phase09Routes inventory checked against service.mount by TestPhase09PermissionMatrix and against admin.json by TestPhase09ContractInventory"
|
||||||
|
pattern: "POST /markdown/preview"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Make the admin SPA's markdown Preview show rendered markdown. The HTML must come from the server's sanitizing renderer, never from the browser. Add an authenticated, CSRF-protected admin route, `POST {prefix}/api/v1/markdown/preview`. It takes `{markdown}`, runs `cabana.RenderMarkdown` (goldmark without unsafe HTML, plus the reject gate) and answers `{html}`. MarkdownField.vue fetches the answer when Preview is shown, fetches again (debounced) when the source changes while Preview is open, and binds only that answer as HTML. mlmarkdown reuses MarkdownField with the active locale's text, so its preview follows the locale.
|
||||||
|
|
||||||
|
Purpose: Journal UAT bug. The Preview tab now shows the raw source (for example `# Title`) inside a `<pre>`.
|
||||||
|
Output: cabana route, handler, swag annotation and Go tests; regenerated admin.json and schema.d.ts; SPA component, styles and vitest tests; cabana README and docs/backend/forms.md; rebuilt modules/boardwalk/dist; raw-HTML hygiene gates taught the one sanctioned binding. Three code commits, one per task.
|
||||||
|
|
||||||
|
Source coverage (from the bug report; there is no CONTEXT/RESEARCH in quick mode):
|
||||||
|
| Item | Task |
|
||||||
|
|------|------|
|
||||||
|
| Preview renders markdown instead of raw source | 2 |
|
||||||
|
| HTML comes only from cabana.RenderMarkdown on the server; no unsanitized HTML in the SPA | 1, 2 |
|
||||||
|
| Authenticated admin endpoint; reuse one if it exists (none exists: RenderMarkdown has no production caller) | 1 |
|
||||||
|
| Fetch on Preview toggle, debounced while Preview is open; raw-HTML binding only on the server's answer | 2 |
|
||||||
|
| Admin auth and CSRF conventions for POST (backend guard group + requireAjax) | 1 |
|
||||||
|
| mlmarkdown preview follows the active locale | 2 |
|
||||||
|
| Go tests: auth required, unsafe HTML stripped | 1 |
|
||||||
|
| SPA vitest tests | 2 |
|
||||||
|
| Rebuild and commit modules/boardwalk/dist with the SPA change | 2 |
|
||||||
|
| README + docs/ for the new route; TestDocsTree green | 1, 2 |
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@~/.claude/gsd-core/workflows/execute-plan.md
|
||||||
|
@~/.claude/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/STATE.md
|
||||||
|
@CLAUDE.md
|
||||||
|
@modules/cabana/field_markdown.go
|
||||||
|
@modules/cabana/csrf.go
|
||||||
|
@admin/src/components/form/fields/MarkdownField.vue
|
||||||
|
@admin/src/components/form/fields/MLMarkdownField.vue
|
||||||
|
@scripts/check-admin-openapi.sh
|
||||||
|
@scripts/check-admin-dist.sh
|
||||||
|
|
||||||
|
Facts gathered while planning (verified against the tree at 0ff928d):
|
||||||
|
|
||||||
|
- `cabana.RenderMarkdown(src) (string, error)` in modules/cabana/field_markdown.go is the only renderer. Goldmark without `html.WithUnsafe` turns raw HTML into `<!-- raw HTML omitted -->` and empties the href of dangerous link URLs. A regex gate then returns an error if the output still contains script/iframe/object/embed tags, `on*=` handlers, or `javascript:`/`vbscript:`/`data:` anywhere in the output. Text that merely mentions `data:` is also refused, so the route must turn a refusal into a 422 rather than a 500. Do NOT change RenderMarkdown or its gate: plugins (the Journal FormatHTML mirror) depend on the same contract.
|
||||||
|
- Admin routes are mounted in `service.mount` (modules/cabana/http.go). Guarded routes live in `r.GroupRaw(api, []string{"backend"}, ...)`. Every state-changing route is wrapped in `requireAjax` (csrf.go): Bearer, or `X-Requested-With: XMLHttpRequest`, else 403 `forbidden` before the body is read. Handlers re-check `bouncer.User(ctx)` with `principal.Backend` and answer 401 `unauthenticated` (see `service.navigation`).
|
||||||
|
- Body decoding: `s.decodeStrictBody(w, r, &dest)` (field_file.go) caps the body at `s.jsonCap()` (http.body_limits.default_bytes, else 1 MiB) and refuses unknown keys and trailing data. `writeCRUDError` maps a `*http.MaxBytesError` to 413 `payload_too_large` and `*ValidationError` to 422 `validation_failed`. `WriteErrorDetails` / `WriteData` (contracts.go) write the D-10 envelopes.
|
||||||
|
- Route inventory tests that must learn the new route: `phase09Routes` in security_coverage_test.go (TestPhase09PermissionMatrix compares it with service.mount and requires the backend guard; TestPhase09ContractInventory requires admin.json to document the path with 200, 401 and BackendBearer security; `AdminAPIRoutes()` in export_test.go feeds it to TestPhase10OpenAPIConformance, which requires exactly one conformance case per route). TestPhase10CSRF in phase10_csrf_test.go walks every POST/PUT/DELETE and asserts the count `unsafe != 25`, which becomes 26.
|
||||||
|
- OpenAPI: swag annotations live in admin_openapi.go (one `func AdminX() {}` per route, request/response types in the same package). `scripts/check-admin-openapi.sh` regenerates admin/openapi/admin.json and admin/src/api/schema.d.ts. With `--check` it is the drift gate. Swag names generic envelopes `cabana.Envelope-cabana_<Type>`.
|
||||||
|
- The SPA makes every HTTP call through the typed `api` client (admin/src/api/client.ts; it already sets X-Requested-With and handles 401 refresh). The phase-10 hygiene rule forbids a direct fetch outside client.ts and any new file under admin/src/api.
|
||||||
|
- MLMarkdownField.vue already renders `<MarkdownField :model-value="activeText">` with the active locale's text, so a preview that re-fetches when its `modelValue` text changes follows the locale with no ML-specific code.
|
||||||
|
- Four gate scripts refuse any raw-HTML sink in admin/src: scripts/check-phase10.sh (hygiene_checks, run from the tree root, output prefix `admin/src/...`, pattern without `=`), check-phase12.1.sh and check-phase12.2.sh (run_hygiene, from ROOT, prefix `admin/src/...`, pattern without `=`) and check-phase14.2.1.sh (run_forbidden, run from `admin/src` with `grep -RInE ... .`, prefix `./...`, pattern with `=`). check-phase12.1/12.2 `--hygiene` fail TODAY only because the MarkdownField.vue line-5 comment contains the directive's name; check-phase14.2.1 `--forbidden` passes today. check-phase10 `--hygiene` also has unrelated pre-existing refusals (admin/src/api/files.ts, untested modules) that this task does not touch. Its `--self-test` plants a binding in `admin/src/__plant/Plant.vue`, which must still be refused.
|
||||||
|
- Tailwind preflight strips heading, list and margin styles, so rendered HTML needs a small style kit. main.css already has the precedent `.summer-partial` in `@layer components`, reading only `--c-*` variables; `--font-mono` exists.
|
||||||
|
- SPA tests: vitest + happy-dom. `mockApi` / `API` (`/admin-test/api/v1`) / `json` / `requestsTo` in admin/tests/helpers.ts mock fetch keyed by "METHOD pathname". MarkdownField.test.ts and MLFields.test.ts ("does not execute raw HTML in the markdown preview") currently assert that the pane shows the source text, and must be rewritten.
|
||||||
|
- Framework hygiene: no consuming-application names (and no Polish catalogue words such as the ones in check-phase10's app-name regex) in admin/src, admin/tests, modules/cabana, the README or docs/. Use neutral `acme`/`Hello` fixtures.
|
||||||
|
- Commits: one logical change each, no co-author trailers, no .planning files in code commits.
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="tracer" tdd="true">
|
||||||
|
<name>Task 1: Server route POST /markdown/preview, end-to-end through guard, CSRF, renderer and OpenAPI contract</name>
|
||||||
|
<files>modules/cabana/field_markdown.go, modules/cabana/http.go, modules/cabana/admin_openapi.go, modules/cabana/markdown_preview_test.go, modules/cabana/markdown_preview_route_test.go, modules/cabana/security_coverage_test.go, modules/cabana/phase10_csrf_test.go, modules/cabana/openapi_conformance_test.go, admin/openapi/admin.json, admin/src/api/schema.d.ts, modules/cabana/README.md, docs/backend/forms.md</files>
|
||||||
|
<read_first>modules/cabana/field_markdown.go, modules/cabana/csrf.go, modules/cabana/http.go (mount, navigation), modules/cabana/field_file.go (jsonCap, decodeStrictBody, invalidBody, AdminFileCaptionRequest), modules/cabana/crud.go (writeCRUDError), modules/cabana/admin_openapi.go (AdminNavigation, AdminSettingsPut, AdminWidgetAction annotations; request type conventions), modules/cabana/security_coverage_test.go (phase09Routes), modules/cabana/phase10_csrf_test.go, modules/cabana/openapi_conformance_test.go (conformCase list, newConformEnv, send), modules/cabana/README.md (Admin API routes table, Features bullet on markdown, API reference rows near cabana.RenderMarkdown), docs/backend/forms.md (Markdown and multilingual fields)</read_first>
|
||||||
|
<precondition>The Docker daemon is reachable (`docker info` succeeds), because the conformance and route tests use testcontainers Postgres.</precondition>
|
||||||
|
<behavior>
|
||||||
|
- Handler with no principal in context: 401, error code `unauthenticated`. Frontend principal (Backend false): 401.
|
||||||
|
- Backend principal, body {"markdown":"# Hello"}: 200, decodes strictly into Envelope[AdminMarkdownPreviewResult], data.html contains `<h1>Hello</h1>`.
|
||||||
|
- Backend principal, body {"markdown":""}: 200 with data.html "".
|
||||||
|
- Unsafe sources, each as its own subtest: a script tag, an iframe tag, an img with an onerror handler, and javascript:, vbscript: and data: link URLs. Either the answer is 200 and the lowercased data.html contains none of `<script`, `<iframe`, `onerror`, `javascript:`, `vbscript:`, `data:`, or it is 422 `validation_failed` with details.markdown. The script-tag case must be 200 with no `<script`: goldmark strips it, which is the "unsafe HTML stripped" proof.
|
||||||
|
- Plain prose whose text contains `data:` (for example "see data: here"): 422 `validation_failed`, details has a `markdown` entry, and the body does not echo the RenderMarkdown error text.
|
||||||
|
- Body {"markdown":"x","extra":1} and malformed JSON: 422 `validation_failed`. A body larger than the cap (service with defaultBytes 64): 413 `payload_too_large`.
|
||||||
|
- Through the assembled router (TestMarkdownPreviewRoute, package cabana_test, newConformEnv plus a login): POST without credentials gives 401. With the Bearer token and a script-tag source it gives 200 with no `<script` in the body.
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Implements the bug report's server requirement: preview HTML comes only from cabana.RenderMarkdown, behind the existing admin auth and CSRF conventions. No route like this exists; RenderMarkdown has no production caller.
|
||||||
|
|
||||||
|
1. modules/cabana/field_markdown.go: add the exported types `AdminMarkdownPreviewRequest` (field `Markdown string` with json tag `markdown`) and `AdminMarkdownPreviewResult` (field `HTML string` with json tag `html`), each with a doc comment. Add `func (s *service) markdownPreview(w http.ResponseWriter, r *http.Request)`:
|
||||||
|
- Re-check `bouncer.User(r.Context())` exactly as `service.navigation` does, answering 401 `unauthenticated` / `msgUnauthenticated`.
|
||||||
|
- Decode with `s.decodeStrictBody(w, r, &body)`. On error, `writeCRUDError(w, err)` (gives 413 or 422).
|
||||||
|
- Call `RenderMarkdown(body.Markdown)`. On error, answer `WriteErrorDetails(w, http.StatusUnprocessableEntity, "validation_failed", "Validation failed", map[string]any{"markdown": []string{<fixed message>}})`. The fixed message says the rendered HTML contains a script or iframe tag, an event handler, or a javascript, vbscript or data URL and cannot be previewed. Never write err.Error().
|
||||||
|
- On success, `WriteData(w, http.StatusOK, AdminMarkdownPreviewResult{HTML: html}, nil)`.
|
||||||
|
- No permission check: any signed-in backend administrator may render markdown. It reads and writes no data (see threat T-261006sne-05).
|
||||||
|
- Leave RenderMarkdown and rejectUnsafeMarkdownHTML unchanged.
|
||||||
|
2. modules/cabana/http.go `service.mount`: inside the `r.GroupRaw(api, []string{"backend"}, ...)` block, right after `g.Get("/navigation", s.navigation)`, add `g.Post("/markdown/preview", requireAjax(s.markdownPreview))`. Add a one-line comment that it renders form previews through RenderMarkdown. No Where constraints: the path has two literal segments and conflicts with no other pattern.
|
||||||
|
3. modules/cabana/admin_openapi.go: add `// AdminMarkdownPreview documents POST /markdown/preview.` followed by swag lines mirroring AdminSettingsPut:
|
||||||
|
- @Summary "Render a markdown preview", and a @Description saying it renders through cabana.RenderMarkdown (goldmark without unsafe HTML), answers the sanitized HTML, gives 422 on `markdown` when the output is refused, and needs any backend session.
|
||||||
|
- @Tags admin, @Accept json, @Produce json, @Security BackendBearer.
|
||||||
|
- `@Param body body AdminMarkdownPreviewRequest true "Markdown source"`.
|
||||||
|
- `@Success 200 {object} Envelope[AdminMarkdownPreviewResult]`.
|
||||||
|
- @Failure 401, 403, 404, 413 and 422 `{object} ErrorEnvelope`.
|
||||||
|
- `@Router /markdown/preview [post]`, then `func AdminMarkdownPreview() {}`.
|
||||||
|
Then run `scripts/check-admin-openapi.sh` (no args) to regenerate admin/openapi/admin.json and admin/src/api/schema.d.ts. Never hand-edit those two files.
|
||||||
|
4. Tests (write them first, RED, then implement):
|
||||||
|
- New modules/cabana/markdown_preview_test.go (package cabana): call `(&service{}).markdownPreview` directly with httptest requests and `bouncer.WithUser`, covering every non-router behavior bullet. Use `&service{defaultBytes: 64}` for the 413 case.
|
||||||
|
- New modules/cabana/markdown_preview_route_test.go (package cabana_test): `TestMarkdownPreviewRoute`, built on `newConformEnv(t)`; log in via `e.send(t, http.MethodPost, "/auth/login", ...)` with `adminTestPassword`, covering the two router bullets.
|
||||||
|
- security_coverage_test.go: add `{key: "POST /markdown/preview"}` to phase09Routes after `PUT /settings/{code}`.
|
||||||
|
- phase10_csrf_test.go: change the expected count from 25 to 26 in both the condition and the message, and add "markdown preview" to the comment list.
|
||||||
|
- openapi_conformance_test.go: add one case to TestPhase10OpenAPIConformance: key "POST /markdown/preview", status 200, ref "cabana.Envelope-cabana_AdminMarkdownPreviewResult", sending {"markdown":"# Conform"} with auth, decoded via `into[cabana.Envelope[cabana.AdminMarkdownPreviewResult]]()`.
|
||||||
|
5. Docs, same commit, per CLAUDE.md:
|
||||||
|
- modules/cabana/README.md: add a row to "Admin API routes": POST `/markdown/preview` renders `{markdown}` through `cabana.RenderMarkdown` for the form preview, answers `{html}`, gives 422 `validation_failed` on `markdown` when the output is refused, and needs any backend session.
|
||||||
|
- README API reference: add rows for `cabana.AdminMarkdownPreviewRequest`, `cabana.AdminMarkdownPreviewResult` and `cabana.AdminMarkdownPreview` (swag annotation).
|
||||||
|
- README Features bullet on markdown fields: add one clause naming the preview route.
|
||||||
|
- docs/backend/forms.md, "Markdown and multilingual fields": add one sentence that `POST <prefix>/api/v1/markdown/preview` renders a source through `cabana.RenderMarkdown` for any signed-in administrator and answers the HTML, or a 422 on `markdown` when the output is refused.
|
||||||
|
- Name no consuming application.
|
||||||
|
6. gofmt, then commit: `feat(cabana): add markdown preview admin route` (Go, tests, admin.json, schema.d.ts, README, forms.md). No .planning files, no trailers.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && bash -c 'test -z "$(gofmt -l modules/cabana)"' && go vet ./modules/cabana/ && go test ./modules/cabana -short -run 'MarkdownPreview|TestMarkdownRejectsUnsafeHTML|TestPhase09|TestPhase10CSRF' && go test ./modules/cabana -run 'TestMarkdownPreviewRoute|TestPhase10OpenAPIConformance' && scripts/check-admin-openapi.sh --check && go test ./cmd/summer -run TestDocsTree</automated>
|
||||||
|
</verify>
|
||||||
|
<done>An authenticated POST to {prefix}/api/v1/markdown/preview with {"markdown":"# Hello"} answers {data:{html:"<h1>Hello</h1>\n"},meta:{}}. Without a session it answers 401; a cookie session without the CSRF header gets 403. Unsafe constructs never survive into a 200 answer. A refused output is 422 on `markdown`. The route is in the inventory, admin.json, schema.d.ts, README and forms.md. One commit.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 2: MarkdownField Preview renders the server-sanitized HTML (markdown and mlmarkdown), rebuilt dist</name>
|
||||||
|
<files>admin/src/components/form/fields/MarkdownField.vue, admin/src/styles/main.css, admin/tests/form/MarkdownField.test.ts, admin/tests/form/MLFields.test.ts, docs/backend/forms.md, modules/boardwalk/dist/</files>
|
||||||
|
<read_first>admin/src/components/form/fields/MarkdownField.vue, admin/src/components/form/fields/MLMarkdownField.vue, admin/src/components/form/control.ts, admin/src/api/client.ts, admin/src/api/schema.d.ts (the /markdown/preview entry from Task 1), admin/src/styles/main.css (the .summer-partial block in @layer components), admin/tests/helpers.ts (mockApi, API, json, requestsTo), admin/tests/form/MarkdownField.test.ts, admin/tests/form/MLFields.test.ts (the markdown preview case), admin/src/components/relation/RelationManager.vue (searchTimer debounce pattern)</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Clicking [data-markdown-preview] with modelValue "# Hello" sends exactly one POST to `${API}/markdown/preview` with JSON body {"markdown":"# Hello"} and the X-Requested-With header. When the mock answers {data:{html:"<h1>Hello</h1>\n"},meta:{}}, the [data-markdown-preview-pane] contains an h1 with text "Hello" and the textarea is gone.
|
||||||
|
- The bound HTML is the server's answer, not the source. Source `<img src=x onerror="window.__md_xss = 1"><script>window.__md_xss = 1</script>` with the mock answering `<p><!-- raw HTML omitted --></p>` leaves no img or script element in the wrapper and window.__md_xss undefined.
|
||||||
|
- A 422 answer (validation_failed, details.markdown ["...refused..."]) shows [data-markdown-preview-error] with that message as text. The pane holds no element children and nothing from the source is rendered as markup.
|
||||||
|
- A network failure (fetch rejects) shows a generic "Preview unavailable." notice. A whitespace-only source sends no request and leaves the pane empty.
|
||||||
|
- Typing in the textarea while Preview is closed sends no request.
|
||||||
|
- With Preview open, a modelValue change triggers one debounced re-fetch (about 300 ms; use fake timers) with the new text. When an older response resolves after a newer one, the older is ignored and the pane shows the newer HTML.
|
||||||
|
- mlmarkdown: with locales en/pl, Preview open on en, switching [data-ml-locale] to pl sends a POST whose body is the pl text, and the pane shows the pl answer.
|
||||||
|
- Clicking Preview again returns to the textarea with the unchanged source.
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Implements the bug report's SPA requirement: Preview renders the server-sanitized HTML, and the preview of an mlmarkdown field follows the active locale.
|
||||||
|
|
||||||
|
1. MarkdownField.vue script:
|
||||||
|
- Import `api` from '../../../api/client'. Keep `rows`, `text`, `attrs` and `preview`.
|
||||||
|
- Add refs: `sanitizedHtml` (string, ''), `previewError` (string, ''), `loading` (boolean). Add a request sequence counter and a debounce timer, using the `ReturnType<typeof setTimeout>` pattern from RelationManager.vue.
|
||||||
|
- `renderPreview()`:
|
||||||
|
- If `text` is empty after trim, clear `sanitizedHtml` and `previewError`, send nothing and return.
|
||||||
|
- Otherwise bump the sequence, set `loading`, and await `api.POST('/markdown/preview', { body: { markdown: text.value } })` inside try/catch.
|
||||||
|
- Ignore any answer whose sequence is no longer current.
|
||||||
|
- On `data`, set `sanitizedHtml` to `data.data.html` and clear `previewError`.
|
||||||
|
- On an error answer, clear `sanitizedHtml` and set `previewError`. Use the first string of `error.error.details.markdown` when it is a string array; otherwise use the fixed text "Preview unavailable.".
|
||||||
|
- On a thrown error, do the same with the fixed text.
|
||||||
|
- Clear `loading` in finally.
|
||||||
|
- Toggling Preview on calls `renderPreview()` immediately. Toggling off cancels the timer.
|
||||||
|
- `watch(text, ...)`: when Preview is open, restart a 300 ms timer that calls `renderPreview()`. When Preview is closed, do nothing.
|
||||||
|
- `onBeforeUnmount` clears the timer.
|
||||||
|
- `sanitizedHtml` is assigned ONLY from `data.data.html`. Never assign the source, an error message or any concatenation to it.
|
||||||
|
2. MarkdownField.vue template:
|
||||||
|
- Keep the Preview button (data-markdown-preview, aria-pressed).
|
||||||
|
- Replace the `<pre>` with a `v-if="preview"` wrapper holding a sibling `<p data-markdown-preview-error role="status">` (text-[13px], danger colour, shown when previewError is set, text interpolation only) and the pane `<div data-markdown-preview-pane>`.
|
||||||
|
- The pane keeps `:class="controlClass(invalid)"` plus `summer-markdown min-h-input overflow-auto px-3.5 py-2.5` and `:aria-busy="loading ? 'true' : 'false'"`. It binds the raw-HTML directive to `sanitizedHtml`. Put that attribute on a line of its own containing exactly the attribute `v-html="sanitizedHtml"` and nothing else, because Task 3's gate exemption matches that exact line.
|
||||||
|
- The textarea branch stays as is (v-else).
|
||||||
|
3. Rewrite the file's head comment. It must say the preview renders only the HTML that POST /markdown/preview answers (cabana.RenderMarkdown: goldmark without unsafe HTML, refused output becomes a 422 notice), and that the markdown source is never bound as HTML. Describe the binding as "the raw-HTML binding". The comment must NOT contain the directive's name or the DOM property names the hygiene gates search for. The current line-5 comment is exactly what makes check-phase12.1/12.2 hygiene fail today.
|
||||||
|
4. main.css, inside `@layer components` after the `.summer-partial` block: add a `.summer-markdown` kit, using `:where()` selectors so specificity stays low and reading only `--c-*` variables and `--font-mono`, in the same style as `.summer-partial`. It covers:
|
||||||
|
- h1 to h4 sizes and weights;
|
||||||
|
- p/ul/ol/blockquote/pre/table margins, with the last child at 0;
|
||||||
|
- ul disc and ol decimal with left padding (preflight removes them);
|
||||||
|
- inline code and pre in `--font-mono` on `--c-subtle` with a radius;
|
||||||
|
- blockquote with a left border in `--c-border-strong` and `--c-muted` text;
|
||||||
|
- links underlined in `--c-text` with a focus-visible ring;
|
||||||
|
- hr and table cell borders in `--c-border`;
|
||||||
|
- images capped at max-width 100%.
|
||||||
|
5. Tests. Rewrite admin/tests/form/MarkdownField.test.ts to cover every behavior bullet with `mockApi` routes keyed `POST ${API}/markdown/preview` (route functions may read `await request.json()` to answer per locale). Use `vi.useFakeTimers()` plus `vi.advanceTimersByTimeAsync` and `flushPromises` for the debounce and race cases, and restore real timers afterwards. In admin/tests/form/MLFields.test.ts, rewrite "does not execute raw HTML in the markdown preview" to mock the route answering `<p><!-- raw HTML omitted --></p>` and assert that no script or img element exists and the window flag is undefined. Drop the assertion that the pane text contains the source. Neutral fixture text only.
|
||||||
|
6. docs/backend/forms.md, "Markdown and multilingual fields": replace "The admin SPA shows a source editor and may preview HTML from `cabana.RenderMarkdown`" with the actual behavior. Preview posts the field's source (the active locale's text for `mlmarkdown`) to the preview route when it opens and again shortly after the source changes. It renders only the server's answer and shows the server's message as text when the output is refused. Keep the existing safety sentence.
|
||||||
|
7. Run `npm --prefix admin run build` and include modules/boardwalk/dist. Commit: `fix(admin): render markdown preview from server-sanitized HTML` (SPA, styles, tests, forms.md, dist). No .planning files, no trailers.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && npm --prefix admin test -- tests/form/MarkdownField.test.ts tests/form/MLFields.test.ts && npm --prefix admin test && npm --prefix admin run typecheck && scripts/check-admin-dist.sh && go test ./cmd/summer -run TestDocsTree</automated>
|
||||||
|
</verify>
|
||||||
|
<done>In the admin SPA, Preview on a markdown field shows rendered headings, lists and links from the server's sanitized HTML. On an mlmarkdown field it shows the active locale and re-renders on a locale switch. A refused output shows a text notice. The source is never bound as HTML. The full vitest suite and typecheck pass, dist matches a fresh build, docs are green. One commit.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Teach the raw-HTML hygiene gates the single sanctioned preview binding; full suite green</name>
|
||||||
|
<files>scripts/check-phase10.sh, scripts/check-phase12.1.sh, scripts/check-phase12.2.sh, scripts/check-phase14.2.1.sh</files>
|
||||||
|
<read_first>scripts/check-phase10.sh (hygiene_checks raw-HTML rule near the "raw-HTML directive" refusal, and the vhtml plant in run_self_test), scripts/check-phase12.1.sh (run_hygiene raw-HTML rule), scripts/check-phase12.2.sh (run_hygiene raw-HTML rule), scripts/check-phase14.2.1.sh (run_forbidden raw-HTML rule)</read_first>
|
||||||
|
<action>
|
||||||
|
Keeps the raw-HTML gates honest now that one server-sanitized binding exists (bug report: "only then uses v-html on the server-sanitized output").
|
||||||
|
<!-- planner-discipline-allow: v-html -->
|
||||||
|
|
||||||
|
1. In each of the four scripts, keep the existing grep. Pipe its output through one `grep -vE` exclusion that drops ONLY the MarkdownField.vue line consisting of optional whitespace, then exactly `v-html="sanitizedHtml"`, then optional whitespace. Insert the exclusion before the `|| true`, matching how check-phase10 already excludes client.ts from the fetch rule.
|
||||||
|
- check-phase10.sh, check-phase12.1.sh and check-phase12.2.sh: anchor at `^admin/src/components/form/fields/MarkdownField\.vue:[0-9]+:`.
|
||||||
|
- check-phase14.2.1.sh: it greps from admin/src with `.`, so anchor at `^\./components/form/fields/MarkdownField\.vue:[0-9]+:`.
|
||||||
|
Use `[[:space:]]*` for the whitespace and escape the dots.
|
||||||
|
2. Add a one-line comment above each exclusion: the markdown preview binds only the HTML that POST /markdown/preview answers (cabana.RenderMarkdown), and every other raw-HTML sink is still refused.
|
||||||
|
3. Leave the patterns themselves, every other rule and check-phase10's self-test plant (admin/src/__plant/Plant.vue) unchanged. The plant must still be refused because the exclusion is file- and line-exact.
|
||||||
|
4. Do not try to fix check-phase10's unrelated pre-existing hygiene refusals (admin/src/api/files.ts, untested modules). Report them in the SUMMARY as pre-existing.
|
||||||
|
5. Commit: `chore(scripts): allow the sanitized markdown preview binding in raw-HTML gates`. No .planning files, no trailers.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && bash -n scripts/check-phase10.sh scripts/check-phase12.1.sh scripts/check-phase12.2.sh scripts/check-phase14.2.1.sh && scripts/check-phase12.1.sh --hygiene && scripts/check-phase12.2.sh --hygiene && scripts/check-phase14.2.1.sh --forbidden && bash -c "! scripts/check-phase10.sh --hygiene 2>&1 | grep -q 'raw-HTML directive'" && go vet ./... && go test ./...</automated>
|
||||||
|
</verify>
|
||||||
|
<done>check-phase12.1 --hygiene and check-phase12.2 --hygiene pass; they failed before this change. check-phase14.2.1 --forbidden still passes. check-phase10 --hygiene no longer reports a raw-HTML directive; its other refusals are pre-existing and listed in the SUMMARY. go vet ./... and go test ./... are green. One commit.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| browser → admin API | Untrusted markdown source (an administrator's input, often translated or pasted) crosses into POST /markdown/preview |
|
||||||
|
| admin API → SPA DOM | Server-rendered HTML is bound as HTML in the admin origin, the first raw-HTML sink in admin/src |
|
||||||
|
| cross-site page → admin API | A foreign page may try to drive the cookie session (CSRF) |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||||
|
| T-261006sne-01 | Tampering (XSS) | MarkdownField.vue preview pane | high | mitigate | Only `data.data.html` of a 2xx answer from POST /markdown/preview is bound. That HTML comes from cabana.RenderMarkdown (goldmark without unsafe HTML plus the reject gate). The source, error messages and concatenations are never bound. Vitest proves that script/img-onerror sources create no elements and do not set the window flag. |
|
||||||
|
| T-261006sne-02 | Tampering (XSS) | service.markdownPreview | high | mitigate | The route always goes through RenderMarkdown. A refused output is a 422 with a fixed message, never the HTML. Go tests cover script, iframe, onerror, javascript:, vbscript: and data: inputs, each asserted either absent from a 200 answer or refused with 422. |
|
||||||
|
| T-261006sne-03 | Spoofing / Elevation | POST /markdown/preview | medium | mitigate | Mounted in the backend-guarded group, and the handler re-checks principal.Backend. TestPhase09PermissionMatrix requires the guard, and the handler and router tests assert 401 without a session. |
|
||||||
|
| T-261006sne-04 | Tampering (CSRF) | POST /markdown/preview | medium | mitigate | Wrapped in requireAjax. TestPhase10CSRF walks it (count 26) and asserts 403 for cookie-only requests with the body unread. |
|
||||||
|
| T-261006sne-05 | Information disclosure | POST /markdown/preview | low | accept | No permission beyond a backend session. The route is stateless: it reads no records and writes nothing, and answers only a rendering of the caller's own input. |
|
||||||
|
| T-261006sne-06 | Denial of service | POST /markdown/preview | low | mitigate | Body capped at http.body_limits.default_bytes (1 MiB default) by decodeStrictBody, giving 413. The SPA fetches on toggle and with a 300 ms debounce only while Preview is open. |
|
||||||
|
| T-261006sne-07 | Tampering (gate erosion) | scripts/check-phase10/12.1/12.2/14.2.1 | medium | mitigate | The exemption matches one file and one exact attribute line. Every other raw-HTML sink, including check-phase10's planted self-test binding, is still refused. |
|
||||||
|
| T-261006sne-SC | Tampering | npm/go installs | low | accept | No new npm or Go dependency. The work uses the existing goldmark pin, openapi-fetch, the pinned swag v1.16.6 tool run and the committed lockfile. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- go vet ./... and go test ./... (Task 3 runs the full suite; testcontainers needs Docker)
|
||||||
|
- go test ./modules/cabana -run 'MarkdownPreview|TestPhase09|TestPhase10CSRF|TestPhase10OpenAPIConformance'
|
||||||
|
- scripts/check-admin-openapi.sh --check
|
||||||
|
- npm --prefix admin test and npm --prefix admin run typecheck
|
||||||
|
- scripts/check-admin-dist.sh
|
||||||
|
- go test ./cmd/summer -run TestDocsTree
|
||||||
|
- scripts/check-phase12.1.sh --hygiene, scripts/check-phase12.2.sh --hygiene, scripts/check-phase14.2.1.sh --forbidden
|
||||||
|
- Manual UAT (not gating): rebuild and restart the proof host, open a markdown field at /backend and click Preview. `# Title` should render as a heading. On an mlmarkdown field, switch the locale with Preview open.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Preview on markdown and mlmarkdown fields shows rendered markdown produced by the server's sanitizing renderer.
|
||||||
|
- No unsanitized HTML reaches the SPA DOM, and the source is never bound as HTML.
|
||||||
|
- The new route follows admin auth (backend guard, 401) and CSRF (requireAjax, 403) conventions. It is documented in OpenAPI, README and docs/, and covered by Go and vitest tests.
|
||||||
|
- modules/boardwalk/dist is rebuilt and committed with the SPA change. All drift gates and go vet / go test ./... are green.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Create `.planning/quick/261006-sne-render-markdown-field-preview-via-saniti/261006-sne-SUMMARY.md` when done
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
---
|
||||||
|
phase: quick-261006-sne
|
||||||
|
plan: 01
|
||||||
|
status: complete
|
||||||
|
subsystem: cabana admin API + admin SPA
|
||||||
|
tags: [markdown, preview, xss, csrf, admin-spa, openapi]
|
||||||
|
requires: [cabana.RenderMarkdown, requireAjax, decodeStrictBody]
|
||||||
|
provides:
|
||||||
|
- POST {prefix}/api/v1/markdown/preview (AdminMarkdownPreviewRequest -> Envelope[AdminMarkdownPreviewResult])
|
||||||
|
- MarkdownField Preview rendering server-sanitized HTML (markdown + mlmarkdown)
|
||||||
|
- .summer-markdown style kit
|
||||||
|
affects: [admin/openapi/admin.json, admin/src/api/schema.d.ts, modules/boardwalk/dist, raw-HTML hygiene gates]
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "Stateless backend-guarded POST behind requireAjax with strict capped body decode"
|
||||||
|
- "SPA request sequence counter + 300 ms debounce; raw-HTML binding fed only from a 2xx data field"
|
||||||
|
- "Hygiene gate exemption matched by file and exact attribute line"
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- modules/cabana/markdown_preview_test.go
|
||||||
|
- modules/cabana/markdown_preview_route_test.go
|
||||||
|
modified:
|
||||||
|
- modules/cabana/field_markdown.go
|
||||||
|
- modules/cabana/http.go
|
||||||
|
- modules/cabana/admin_openapi.go
|
||||||
|
- modules/cabana/security_coverage_test.go
|
||||||
|
- modules/cabana/phase10_csrf_test.go
|
||||||
|
- modules/cabana/phase10_coverage_test.go
|
||||||
|
- modules/cabana/openapi_conformance_test.go
|
||||||
|
- modules/cabana/README.md
|
||||||
|
- admin/openapi/admin.json
|
||||||
|
- admin/src/api/schema.d.ts
|
||||||
|
- admin/src/components/form/fields/MarkdownField.vue
|
||||||
|
- admin/src/styles/main.css
|
||||||
|
- admin/tests/form/MarkdownField.test.ts
|
||||||
|
- admin/tests/form/MLFields.test.ts
|
||||||
|
- docs/backend/forms.md
|
||||||
|
- modules/boardwalk/dist/
|
||||||
|
- scripts/check-phase10.sh
|
||||||
|
- scripts/check-phase12.1.sh
|
||||||
|
- scripts/check-phase12.2.sh
|
||||||
|
- scripts/check-phase14.2.1.sh
|
||||||
|
decisions:
|
||||||
|
- "Markdown preview is a stateless POST /markdown/preview open to any backend session (no permission code); refused output is a 422 on markdown with a fixed message, never the renderer error text"
|
||||||
|
- "The SPA binds only data.data.html of a 2xx preview answer; refusals and network errors are text notices"
|
||||||
|
- "Raw-HTML hygiene gates exempt exactly one line: MarkdownField.vue `v-html=\"sanitizedHtml\"`"
|
||||||
|
metrics:
|
||||||
|
duration: 11m
|
||||||
|
completed: 2026-10-06
|
||||||
|
tasks: 3
|
||||||
|
files: 25
|
||||||
|
actuals:
|
||||||
|
tokens: 15800
|
||||||
|
tasks: 3
|
||||||
|
commits: 3
|
||||||
|
commits: 3
|
||||||
|
plan_head_before: 0ff928d6cf7ee6f43b2457c99ef401cb1855f535
|
||||||
|
plan_head_after: 27711b7c21a8545594fa9d548925d599bb91a237
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quick 261006-sne: Render markdown field preview via sanitized server HTML Summary
|
||||||
|
|
||||||
|
The admin markdown Preview now renders headings, lists and links. The HTML comes from a new backend-guarded, CSRF-protected `POST {prefix}/api/v1/markdown/preview` route that runs `cabana.RenderMarkdown`. MarkdownField binds only that answer, and on mlmarkdown fields the preview follows the active locale.
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
| Task | Name | Commit | Key files |
|
||||||
|
|------|------|--------|-----------|
|
||||||
|
| 1 | Server route POST /markdown/preview (tracer) | b492e79 | field_markdown.go, http.go, admin_openapi.go, Go tests, admin.json, schema.d.ts, README, forms.md |
|
||||||
|
| 2 | MarkdownField Preview renders server-sanitized HTML, rebuilt dist | a0116df | MarkdownField.vue, main.css, MarkdownField.test.ts, MLFields.test.ts, forms.md, modules/boardwalk/dist |
|
||||||
|
| 3 | Raw-HTML gates allow the single sanctioned binding | 27711b7 | check-phase10/12.1/12.2/14.2.1.sh |
|
||||||
|
|
||||||
|
## What was built
|
||||||
|
|
||||||
|
- **Route:** `service.markdownPreview` re-checks the backend principal and answers 401 `unauthenticated` without one. It decodes `{markdown}` through `decodeStrictBody`: unknown keys, malformed JSON and trailing data get 422, and a body over the cap gets 413. It renders through `RenderMarkdown` and answers `{html}`. When the gate refuses the output, the answer is 422 `validation_failed` with a fixed message on `markdown`. The route is mounted in the `backend` guard group behind `requireAjax`. `RenderMarkdown` and its gate are unchanged.
|
||||||
|
- **Tracer gate:** the route ran end to end through guard, CSRF, renderer and OpenAPI conformance (`TestMarkdownPreviewRoute`, `TestPhase10OpenAPIConformance`) before the SPA work started.
|
||||||
|
- **SPA:** Preview fetches when it opens. While it is open, a source change triggers one re-fetch after 300 ms. A sequence counter drops stale answers. Blank sources send nothing. Error answers show the server's `details.markdown[0]` as text, and failed requests show "Preview unavailable.". `aria-busy` reflects loading.
|
||||||
|
- **Styles:** `.summer-markdown` style kit with low-specificity `:where()` selectors that read only `--c-*` variables and `--font-mono`.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `go vet ./...` and `go test ./...` passed before each of the three commits (testcontainers Postgres included).
|
||||||
|
- `scripts/check-admin-openapi.sh --check`, `scripts/check-admin-dist.sh` and `go test ./cmd/summer -run TestDocsTree` all pass.
|
||||||
|
- `npm --prefix admin test` passes (73 files, 1054 tests), and `npm --prefix admin run typecheck` is clean.
|
||||||
|
- `check-phase12.1.sh --hygiene` and `check-phase12.2.sh --hygiene` now pass; both failed before this change. `check-phase14.2.1.sh --forbidden` passes. `check-phase10.sh --hygiene` no longer reports a raw-HTML directive.
|
||||||
|
- The exemption was tested against the planted `<div v-html="raw" />` and against the variants `<div v-html="sanitizedHtml" />` and `v-html="other"` in MarkdownField.vue, using the real GNU grep. All three are still refused.
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Rule 3 - Blocking] Second route inventory in phase10_coverage_test.go**
|
||||||
|
- **Found during:** Task 1 (full `go test ./...`)
|
||||||
|
- **Issue:** `TestPhase10Coverage/every_unsafe_mounted_route_is_CSRF-walked` keeps its own sorted list of unsafe routes and says "expects 25". The plan did not list this file.
|
||||||
|
- **Fix:** Added `POST /markdown/preview` to the list in sort order and changed the message to 26.
|
||||||
|
- **Files modified:** modules/cabana/phase10_coverage_test.go
|
||||||
|
- **Commit:** b492e79
|
||||||
|
|
||||||
|
**2. [Rule 1 - Bug] vue-tsc noUncheckedIndexedAccess in the new vitest file**
|
||||||
|
- **Found during:** Task 2 typecheck
|
||||||
|
- **Fix:** Added non-null assertions on indexed `sent[n]` / `pending[n]` accesses in MarkdownField.test.ts.
|
||||||
|
- **Commit:** a0116df
|
||||||
|
|
||||||
|
Small additions beyond the plan's behavior list:
|
||||||
|
- A trailing-data case in the Go invalid-body test.
|
||||||
|
- Closing Preview or unmounting bumps the sequence, so an answer still in flight is dropped.
|
||||||
|
|
||||||
|
## Pre-existing issues (not touched)
|
||||||
|
|
||||||
|
These `scripts/check-phase10.sh --hygiene` refusals predate this task and are out of scope:
|
||||||
|
- `admin/src/api/files.ts:150` uses `new XMLHttpRequest()`, which the direct-fetch rule catches.
|
||||||
|
- `admin/src/api/files.ts` is an unexpected file in `admin/src/api`.
|
||||||
|
- `admin/src/components/form/fields/FileCaptionModal.vue` is imported by no test.
|
||||||
|
- `admin/src/components/form/mlLocale.ts` is imported by no test.
|
||||||
|
|
||||||
|
Because of them, `scripts/check-phase10.sh --self-test` stops with "rejected the clean scratch copy" before it plants anything. The plant refusal was checked by hand instead (see Verification).
|
||||||
|
|
||||||
|
## Threat Flags
|
||||||
|
|
||||||
|
None beyond the plan's threat model. The one new surface, the T-261006sne-01..07 route and the raw-HTML sink, is mitigated as the plan specifies.
|
||||||
|
|
||||||
|
## Known Stubs
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Manual UAT (not gating)
|
||||||
|
|
||||||
|
1. Rebuild and restart the proof host.
|
||||||
|
2. Open a markdown field at /backend and click Preview. `# Title` should render as a heading.
|
||||||
|
3. Open an mlmarkdown field, click Preview and switch the locale. The preview should follow the locale.
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- FOUND: modules/cabana/markdown_preview_test.go, modules/cabana/markdown_preview_route_test.go, admin/src/components/form/fields/MarkdownField.vue
|
||||||
|
- FOUND commits: b492e79, a0116df, 27711b7
|
||||||
Reference in New Issue
Block a user