Skip to content

feat(ogar-render-askama): T3 — HtmlDetailView (Redmine show.html.erb on ClassView)#84

Merged
AdaWorldAPI merged 2 commits into
mainfrom
claude/ogar-render-askama-t3-html-detail-view
Jun 20, 2026
Merged

feat(ogar-render-askama): T3 — HtmlDetailView (Redmine show.html.erb on ClassView)#84
AdaWorldAPI merged 2 commits into
mainfrom
claude/ogar-render-askama-t3-html-detail-view

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

What

T3 per Northstar plan §3 — the detail-page sibling of T2 (#83). Mirrors Redmine's app/views/issues/show.html.erb shape: <dl> for inline typed fields + full-width <section> blocks for prose / family-edge collections.

Stacks on top of #83. T3 reuses every piece T2 built — RenderColumn, ColumnKind, the 10 cell sub-templates, the two-stage cells-pre-rendered-in-Rust pattern. T3 is just a different spine template; the substrate-agnostic property holds.

Surface

API Purpose
ArtifactKind::HtmlDetailView New variant at slot 2 (append-only)
dispatch/html_detail_view.askama Spine template (<article> + header + <dl> + per-block <section>s)
artifact_kinds::html_detail_view::render_detail(...) Substantive entry point
HtmlDetailViewEmitter Codebook-only proof-of-shape for for_kind dispatch
let html = render_detail(
    /* class_id      */ 0x0102,
    /* concept       */ "project_work_item",
    /* record_id     */ 42,
    /* headline_html */ "<a href=\"/issues/42\" class=\"primary-link\">Fix the foo</a>",
    /* subtitle      */ "Open · High",
    /* columns       */ &[status_col, done_ratio_col, description_col.block()],
    /* cells         */ &[primary_link_cell, progress_bar_cell, rich_text_cell],
)?;

Contract

columns.len() == cells.len() — paired by index. The emitter returns askama::Error on mismatch (caller-side bug, pinned by html_detail_view_column_cell_arity_mismatch_returns_error).

Block columns (inline=false) get full-width <section>s below the <dl>; inline columns stay in the <dl>. The render-time split is by column.inline.

Tests (23/23, +3 T3-specific)

  • html_detail_view_proof_of_shape_renders_dl_with_class_meta — codebook emit on project_work_item; pins data-class-id / data-concept / detail-field-<name> per attribute.
  • html_detail_view_renders_inline_dl_and_block_sections — substantive render with PrimaryLink + ProgressBar inline + RichText block. Pins record id, headline, subtitle, per-kind cell sub-templates, block <section> for description, and a negative assert that block content didn't leak into the inline <dl>.
  • html_detail_view_column_cell_arity_mismatch_returns_error — caller-side contract.

Workspace check + workspace test green.

What this collapses (Redmine show.html.erb · 179 LOC vs ours)

Redmine T3
show.html.erb per controller (~26 of them, mostly 100–200 LOC each) 1 spine template, 34 LOC
Hand-laid HTML around helper calls Substrate-agnostic <dl> + sections
Conditional sections per resource (subtasks, relations, watchers, journals…) Block columns; same RenderColumn plumbing as the list view
Per-resource _attributes.html.erb partial Inline cells of the unified pipeline

Notes

Cell-renderer dispatch is currently duplicated between html_list_view::render_cell and html_detail_view::render_cell_body. Once T4 (HtmlForm) lands, that becomes the third call site and warrants factoring into one helper in artifact_kinds::cells. Not factoring prematurely — three points form a line.

Follow-on

T4 (HtmlForm) — same plumbing, different artifact (create/edit form with typed <input> per attribute keyed off ColumnKind). The factoring of cell-dispatch goes with T4.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bafb27e03c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// ── Spine binding struct ─────────────────────────────────────────────

#[derive(Template)]
#[template(path = "dispatch/html_list_view.askama", escape = "none")]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Re-enable HTML escaping for list spine

Because this is an HTML view but the spine is compiled with escape = "none", every interpolation in html_list_view.askama is raw unless manually filtered. In the normal row-rendering path, values such as the saved/list title, column captions, and especially grouped row labels can come from data or user-visible configuration; a label like <script>…</script> will be emitted directly. Keep the template on HTML escaping and mark only the pre-rendered cell.body_html as safe.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2b3fb88. Both spines now compile with escape = "html"; only the intentionally-pre-rendered fields (headline_html, cell.body_html, section.body_html) bypass via |safe. Two XSS regression tests added — list + detail variants — pinning that every data-derived slot (title, subtitle, labels, captions, css_classes, group headers) escapes to &lt;...&gt; while the intentional HTML (<a>...</a> headline, rich-text section body) survives. escape = "none" retained only on rust_struct.askama (emits Rust source, not HTML) and cell/rich_text.askama (trusted pre-rendered prose). 25/25 tests green; thanks for the catch.

// ── Spine binding struct ─────────────────────────────────────────────

#[derive(Template)]
#[template(path = "dispatch/html_detail_view.askama", escape = "none")]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Re-enable HTML escaping for detail spine

This detail view also emits browser-facing HTML while disabling Askama escaping globally, so ordinary fields like subtitle, section labels, and inline field labels are rendered raw in html_detail_view.askama. When those values are derived from record data or late-resolved labels, malicious markup is injected into the page; only the intentionally pre-rendered headline_html/cell bodies should be marked safe while the rest stays HTML-escaped.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2b3fb88. Both spines now compile with escape = "html"; only the intentionally-pre-rendered fields (headline_html, cell.body_html, section.body_html) bypass via |safe. Two XSS regression tests added — list + detail variants — pinning that every data-derived slot (title, subtitle, labels, captions, css_classes, group headers) escapes to &lt;...&gt; while the intentional HTML (<a>...</a> headline, rich-text section body) survives. escape = "none" retained only on rust_struct.askama (emits Rust source, not HTML) and cell/rich_text.askama (trusted pre-rendered prose). 25/25 tests green; thanks for the catch.

claude added 2 commits June 20, 2026 01:06
…on ClassView)

Third real emitter per Northstar plan §3, the detail-page sibling of T2's
HtmlListView. Mirrors Redmine's `app/views/issues/show.html.erb` shape:
definition-list (dt/dd) for inline typed fields + full-width <section>
blocks for prose / family-edge collections.

Reuses every piece T2 built — RenderColumn, ColumnKind, the 10 cell sub-
templates, the two-stage cells-pre-rendered-in-Rust pattern. T3 is just
a different spine template; the substrate-agnostic property holds.

Surface:
- ArtifactKind::HtmlDetailView variant (slot 2, append-only).
- templates/dispatch/html_detail_view.askama — spine (<article> + header
  + <dl class="detail-fields"> + per-block <section>s).
- artifact_kinds::html_detail_view::render_detail(class_id, concept,
  record_id, headline_html, subtitle, columns, cells) — the real entry
  point.
- HtmlDetailViewEmitter — codebook-only proof-of-shape for the for_kind
  dispatch path.

Arity contract: `columns.len() == cells.len()`; the emitter returns an
askama::Error on mismatch (caller-side bug, pinned by test).

Tests (+3 T3 specific, 23/23 total):
- html_detail_view_proof_of_shape_renders_dl_with_class_meta — codebook
  emit on project_work_item; surfaces data-class-id + data-concept +
  detail-field-<name> per attribute.
- html_detail_view_renders_inline_dl_and_block_sections — substantive
  render with PrimaryLink + ProgressBar inline + RichText block;
  pins record-id, headline, subtitle, inline cells via per-kind
  sub-templates, block <section> for description, and a NEGATIVE assert
  that the block didn't leak into the inline <dl>.
- html_detail_view_column_cell_arity_mismatch_returns_error — caller-
  side contract.

Workspace check + test green. Cell-renderer dispatch duplicated between
list_view and detail_view emitters today; once T4 (HtmlForm) lands we
factor it into one helper. Both pipelines feed the same per-kind
sub-templates from artifact_kinds::cells.
…templates

Codex P1 on PR #83 (list spine) + PR #84 (detail spine), same root issue:
both spine templates were compiled with `escape = "none"`, so EVERY
interpolation was raw — a malicious group-header label, title, caption,
subtitle, or css_class would inject raw HTML into the page (XSS hazard).

Fix: `escape = "html"` on the spine binding-struct attribute. The
templates already use `|safe` correctly on the intentionally-pre-rendered
HTML fields (`cell.body_html`, `headline_html`, `section.body_html`), so
only those bypass escaping. Every other interpolation (title, captions,
labels, css_classes, group header labels, subtitle) now gets HTML-escaped
by askama.

Two-layer escaping for cell content is consistent:
- The cell sub-template (e.g. PrimaryLinkCell) escapes its variables
  (label, href, …) under its own `escape = "html"`.
- The spine receives the already-escaped HTML as `body_html` and
  passes it through with `|safe` (no double-escape).

`escape = "none"` retained only on:
- `rust_struct.askama` — emits Rust source where `<` etc. must NOT be
  HTML-escaped.
- `cell/rich_text.askama` — wraps trusted pre-rendered prose
  (Markdown/Textile already expanded upstream).

Tests (+2 XSS regressions, 25/25 total):
- html_list_view_escapes_data_derived_strings_xss_regression — pokes
  every untrusted slot (title, caption, group label, css_classes) with
  XSS payloads (`<script>`, `<img src=x onerror=…>`) + a `<malicious-class>`
  tag; asserts none survive raw, all escape to `&lt;...&gt;`. Also pins
  that the PrimaryLink cell's label `<safe>label</safe>` becomes
  `&lt;safe&gt;label&lt;/safe&gt;` (per-cell escape) and that the
  intentional cell HTML (`<a href="/i/1">`) DOES survive (cell body |safe).
- html_detail_view_escapes_data_derived_strings_xss_regression —
  parallel set: subtitle, inline + block labels, cell + section css.
  Pins that headline_html (`<a>...</a>`) and rich-text section body
  (`<p>...</p>`) pass through (marked safe), while every data-derived
  string is escaped.

Workspace check + test green. T3 (HtmlDetailView from #84) folded
into this commit since the cherry-picked T2 commit was dropped on
rebase against the merged #83 main; this PR delivers both the T3 feature
AND the codex P1 fix for the now-merged T2 spine.
@AdaWorldAPI AdaWorldAPI force-pushed the claude/ogar-render-askama-t3-html-detail-view branch from bafb27e to 2b3fb88 Compare June 20, 2026 01:09
@AdaWorldAPI AdaWorldAPI merged commit 410779b into main Jun 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants