Files
summercms/.planning/quick/261006-sne-render-markdown-field-preview-via-saniti/261006-sne-SUMMARY.md

7.5 KiB

phase, plan, status, subsystem, tags, requires, provides, affects, tech-stack, key-files, decisions, metrics, actuals, commits, plan_head_before, plan_head_after
phase plan status subsystem tags requires provides affects tech-stack key-files decisions metrics actuals commits plan_head_before plan_head_after
quick-261006-sne 01 complete cabana admin API + admin SPA
markdown
preview
xss
csrf
admin-spa
openapi
cabana.RenderMarkdown
requireAjax
decodeStrictBody
POST {prefix}/api/v1/markdown/preview (AdminMarkdownPreviewRequest -> Envelope[AdminMarkdownPreviewResult])
MarkdownField Preview rendering server-sanitized HTML (markdown + mlmarkdown)
.summer-markdown style kit
admin/openapi/admin.json
admin/src/api/schema.d.ts
modules/boardwalk/dist
raw-HTML hygiene gates
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
created modified
modules/cabana/markdown_preview_test.go
modules/cabana/markdown_preview_route_test.go
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
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"`
duration completed tasks files
11m 2026-10-06 3 25
tokens tasks commits
15800 3 3
3 0ff928d6cf 27711b7c21

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