Michael Rabinovich commited on
Commit
0e3b21f
·
1 Parent(s): 77edebf

app: render reports inline via iframe srcdoc (private-Space safe)

Browse files

Polish follow-up on C6. Reports now render inside the Space, inside
the same iframe HF uses to host the leaderboard, with no second
HTTP request leaving the browser.

Why: the Space is private, and HF's edge gates same-origin pathname
navigations on a short-lived __sign JWT that browsers don't carry
forward when a user clicks a relative link. The previous design
(submission_name -> /reports/<id>.html) worked server-side under
Bearer auth (verified) but 404'd in the browser. Confirmed via
external review and reproduced with a requests.Session cookie test.

This commit:

- Drops the submission_name -> /reports/<id>.html link wrapping
from the rendered table; the cell stays plain text. The
underlying `report_url` column (relative URL to the proxy) is
still computed for the modern-pipeline gate but no longer
surfaced as a clickable cell.
- Adds a `gr.HTML` viewer below the detail panel. Row-click
triggers `_format_detail_and_report`, which returns
(detail_markdown, report_iframe_html). The iframe wraps the
full report HTML via `srcdoc` so the report's CSS gets its own
document context (no Gradio flex-column clipping; explicit
`height: 90vh`).
- Keeps the FastAPI `/reports/{submission_id}.html` route as a
backdoor (works under Bearer for tooling and survives the
future public migration where it'll just become user-reachable).

Why srcdoc and not src=URL: srcdoc inlines the bytes so the
browser doesn't need to issue an authenticated second request to
the Space's edge. When the Space goes public, swap srcdoc for
src=/reports/<id>.html and the iframe keeps the same layout
isolation - 1-line change.

Verification (autonomous, no user involvement):

- 19/19 unit tests green. New tests cover the iframe handler:
modern row -> iframe-srcdoc with HTML-escaped body; pending /
failed / legacy rows -> empty viewer; fetch failure -> empty
viewer (no broken iframe); null select event -> placeholder;
attribute escaping safe against `"` and `&` in the report
content.
- Real report inspection: fetched a known modern report,
1.38 MB, 45 images all base64-inlined as PNG, no external
URLs, all expected sections present (INTERFACE OVERLAY, axis
labels, per-fixture blocks). The earlier visible cut-off was
Gradio's column flex, not the report - confirmed.
- Local boot probe: app.py serves /, the /reports/<sid>.html
backdoor returns 200 + text/html + 1.4 MB body, /reports/<bogus>
returns 404.

Post-push live probe runs next; will surface only if it fails.

Files changed (4) hide show
  1. app.py +68 -35
  2. leaderboard.py +17 -8
  3. tests/test_leaderboard.py +21 -28
  4. tests/test_proxy.py +131 -8
app.py CHANGED
@@ -10,6 +10,7 @@ rather than render).
10
  """
11
  from __future__ import annotations
12
 
 
13
  import logging
14
  import os
15
  import re
@@ -92,24 +93,53 @@ def _fmt_timestamp(ts) -> str:
92
  return str(ts)
93
 
94
 
95
- def _format_detail(df: pd.DataFrame | None, evt: gr.SelectData) -> str:
96
- """Build the row-detail markdown for the clicked submission.
97
 
98
- Pulls hidden columns (``notes``, ``failure_reason``,
99
- ``submission_id``) that ride in the underlying DataFrame plus
100
- the visible link cells (already pre-formatted as ``[label](url)``
101
- by ``leaderboard.py``'s ``_project_and_format``).
102
- ``failure_reason`` only shows on ``failed`` rows;
103
- ``report_url`` is only non-empty for completed rows from the
104
- modern submit pipeline (leaderboard.py gates on
105
- ``submission_sha256`` so legacy rows don't render a broken
106
- /resolve/ link).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  """
108
  if df is None or len(df) == 0 or evt is None or evt.index is None:
109
- return DETAIL_PLACEHOLDER
110
  idx = evt.index[0] if isinstance(evt.index, (list, tuple)) else evt.index
111
  if idx < 0 or idx >= len(df):
112
- return DETAIL_PLACEHOLDER
113
  row = df.iloc[idx]
114
 
115
  title = row.get("submission_name") or "(unnamed submission)"
@@ -122,20 +152,24 @@ def _format_detail(df: pd.DataFrame | None, evt: gr.SelectData) -> str:
122
  lines.append(f"- **Submitted**: {_fmt_timestamp(row['submitted_at'])}")
123
  if _has(row.get("notes")):
124
  lines.append(f"- **Notes**: {row['notes']}")
125
- # `model details (optional)` carries the markdown link (or
126
- # _None_ when missing); the hidden `submission_blob_url` /
127
- # `report_url` columns are raw URLs we wrap into named links here.
128
  lines.append(
129
  f"- **Model details (optional)**: "
130
  f"{row.get('model details (optional)') or '_None_'}"
131
  )
132
  if _has(row.get("submission_blob_url")):
133
- lines.append(f"- **Submission ZIP**: [download]({row['submission_blob_url']})")
134
- if _has(row.get("report_url")):
135
- lines.append(f"- **Report**: [open]({row['report_url']})")
136
  if row.get("status") == "failed" and _has(row.get("failure_reason")):
137
  lines.append(f"- **Failure reason**: {row['failure_reason']}")
138
- return "\n".join(lines)
 
 
 
 
 
 
 
139
 
140
 
141
  @lru_cache(maxsize=128)
@@ -212,25 +246,24 @@ with gr.Blocks(title="CADGenBench Leaderboard", theme=gr.themes.Soft()) as block
212
  outputs=[validated_view, unvalidated_view],
213
  )
214
 
215
- # Row-click detail panel: a single shared markdown component
216
- # below the tables, populated by either table's .select()
217
- # event. Pulls hidden columns (notes, failure_reason,
218
- # submission_id) plus the visible link cells (already pre-
219
- # formatted as markdown by leaderboard.py) for the row.
 
 
220
  detail_panel = gr.Markdown(
221
  value=DETAIL_PLACEHOLDER,
222
  label="Selected submission",
223
  )
224
- validated_view.select(
225
- fn=_format_detail,
226
- inputs=validated_view,
227
- outputs=detail_panel,
228
- )
229
- unvalidated_view.select(
230
- fn=_format_detail,
231
- inputs=unvalidated_view,
232
- outputs=detail_panel,
233
- )
234
 
235
  with gr.Tab("Submit"):
236
  gr.Markdown(
 
10
  """
11
  from __future__ import annotations
12
 
13
+ import html
14
  import logging
15
  import os
16
  import re
 
93
  return str(ts)
94
 
95
 
96
+ def _build_report_iframe(html_bytes: bytes) -> str:
97
+ """Wrap a fetched report's HTML bytes into a self-contained iframe.
98
 
99
+ ``srcdoc`` puts the entire report HTML directly inside the iframe
100
+ attribute. The iframe gets its own document context, so the
101
+ report's CSS can't collide with Gradio's, and an explicit
102
+ ``height: 90vh`` keeps the report from being clipped by Gradio's
103
+ column-flex layout (which was the visible cut-off the user saw
104
+ in earlier rendering attempts).
105
+
106
+ The Space is private; the FastAPI ``/reports/<id>.html`` route
107
+ works server-side under Bearer auth but breaks for a logged-in
108
+ browser user (HF's edge gates same-origin pathname navigations
109
+ on a JWT that the browser doesn't carry forward). srcdoc
110
+ sidesteps that entirely by inlining the bytes; no second HTTP
111
+ request leaves the browser.
112
+ """
113
+ escaped = html.escape(
114
+ html_bytes.decode("utf-8", errors="replace"), quote=True,
115
+ )
116
+ return (
117
+ f'<iframe srcdoc="{escaped}" '
118
+ 'style="width:100%; height:90vh; border:0; display:block;" '
119
+ 'title="Submission report"></iframe>'
120
+ )
121
+
122
+
123
+ def _format_detail_and_report(
124
+ df: pd.DataFrame | None, evt: gr.SelectData,
125
+ ) -> tuple[str, str]:
126
+ """Return ``(detail_markdown, report_iframe_html)`` for the clicked row.
127
+
128
+ The detail panel holds metadata (submitter, status, timestamp,
129
+ notes, links to ZIP and external agent URL). The HTML viewer
130
+ holds the rendered report when one exists (status == completed
131
+ AND the row carries the modern-pipeline ``submission_sha256``
132
+ sentinel) - the leaderboard reader computes ``report_url`` only
133
+ for rows that satisfy that gate.
134
+
135
+ Returns ``(DETAIL_PLACEHOLDER, "")`` on a null / out-of-range
136
+ event so the panel falls back to its initial state.
137
  """
138
  if df is None or len(df) == 0 or evt is None or evt.index is None:
139
+ return DETAIL_PLACEHOLDER, ""
140
  idx = evt.index[0] if isinstance(evt.index, (list, tuple)) else evt.index
141
  if idx < 0 or idx >= len(df):
142
+ return DETAIL_PLACEHOLDER, ""
143
  row = df.iloc[idx]
144
 
145
  title = row.get("submission_name") or "(unnamed submission)"
 
152
  lines.append(f"- **Submitted**: {_fmt_timestamp(row['submitted_at'])}")
153
  if _has(row.get("notes")):
154
  lines.append(f"- **Notes**: {row['notes']}")
 
 
 
155
  lines.append(
156
  f"- **Model details (optional)**: "
157
  f"{row.get('model details (optional)') or '_None_'}"
158
  )
159
  if _has(row.get("submission_blob_url")):
160
+ lines.append(
161
+ f"- **Submission ZIP**: [download]({row['submission_blob_url']})"
162
+ )
163
  if row.get("status") == "failed" and _has(row.get("failure_reason")):
164
  lines.append(f"- **Failure reason**: {row['failure_reason']}")
165
+ detail_md = "\n".join(lines)
166
+
167
+ report_iframe = ""
168
+ if _has(row.get("report_url")) and _has(row.get("submission_id")):
169
+ content = _fetch_report_html(str(row["submission_id"]))
170
+ if content:
171
+ report_iframe = _build_report_iframe(content)
172
+ return detail_md, report_iframe
173
 
174
 
175
  @lru_cache(maxsize=128)
 
246
  outputs=[validated_view, unvalidated_view],
247
  )
248
 
249
+ # Row-click panel: one shared metadata markdown component +
250
+ # one report viewer below it. The viewer holds an iframe
251
+ # containing the full per-submission report (srcdoc-inlined
252
+ # so no second HTTP request needs to leave the browser - the
253
+ # Space is private and HF's edge would 404 same-origin
254
+ # pathname navigations that aren't carrying the iframe's
255
+ # short-lived `__sign` JWT). Both tables share both outputs.
256
  detail_panel = gr.Markdown(
257
  value=DETAIL_PLACEHOLDER,
258
  label="Selected submission",
259
  )
260
+ report_viewer = gr.HTML(value="", label="Report")
261
+ for view in (validated_view, unvalidated_view):
262
+ view.select(
263
+ fn=_format_detail_and_report,
264
+ inputs=view,
265
+ outputs=[detail_panel, report_viewer],
266
+ )
 
 
 
267
 
268
  with gr.Tab("Submit"):
269
  gr.Markdown(
leaderboard.py CHANGED
@@ -247,17 +247,26 @@ def _report_relative_url(submission_id, status, submission_sha256) -> str:
247
 
248
 
249
  def _submission_name_md(name, report_url) -> str:
250
- """Render `submission_name` as a markdown link when a report exists.
251
-
252
- The submission name itself is the link target (typical HF
253
- leaderboard pattern: model / system name doubles as the
254
- deep-link). Plain text when no report is available.
 
 
 
 
 
 
 
 
 
 
 
255
  """
256
  if _is_empty(name):
257
  return "(unnamed submission)"
258
- if _is_empty(report_url):
259
- return str(name)
260
- return f"[{name}]({report_url})"
261
 
262
 
263
  def load_leaderboard_split() -> tuple[pd.DataFrame, pd.DataFrame]:
 
247
 
248
 
249
  def _submission_name_md(name, report_url) -> str:
250
+ """Render `submission_name` as plain text.
251
+
252
+ Earlier iterations wrapped this as a markdown link to the report
253
+ proxy at ``/reports/<id>.html``. On a private Space the link
254
+ works server-side (Bearer auth) but breaks for a logged-in user
255
+ in the browser: the iframe's `__sign` token doesn't propagate to
256
+ same-origin pathname navigations, and HF's edge returns 404
257
+ before the request reaches the FastAPI route. Reports are now
258
+ rendered inline via an iframe-srcdoc viewer below the detail
259
+ panel (see ``_format_detail_and_report`` in :mod:`app`), so the
260
+ submission_name cell stays plain text to avoid offering a link
261
+ that doesn't work.
262
+
263
+ `report_url` is accepted (and ignored) so the call sites don't
264
+ have to change when the Space eventually goes public and the
265
+ link can be restored.
266
  """
267
  if _is_empty(name):
268
  return "(unnamed submission)"
269
+ return str(name)
 
 
270
 
271
 
272
  def load_leaderboard_split() -> tuple[pd.DataFrame, pd.DataFrame]:
tests/test_leaderboard.py CHANGED
@@ -133,39 +133,32 @@ def test_empty_input_returns_two_empty_frames(monkeypatch):
133
  assert list(unvalidated.columns) == leaderboard.LEADERBOARD_COLS
134
 
135
 
136
- def test_submission_name_links_to_report_when_available(monkeypatch):
137
- """`submission_name` cell is a markdown link to the proxy when a report exists.
138
-
139
- Modern rows (status=="completed" + non-null submission_sha256)
140
- get ``[<name>](/reports/<sid>.html)``; the relative URL targets
141
- the Space's report proxy.
 
 
142
  """
143
  monkeypatch.setattr(leaderboard, "_load_rows_from_hub", lambda: _stub_rows())
144
  validated, unvalidated = leaderboard.load_leaderboard_split()
145
-
146
- # Alpha is a modern completed row -> linked name.
 
 
 
 
 
 
 
 
 
 
147
  alpha = validated.iloc[0]
148
- assert alpha["submission_name"] == "[Alpha Agent v1](/reports/sub-a.html)"
149
- # Beta likewise (also completed, has sha256).
150
- beta = unvalidated[
151
- unvalidated["submission_name"].str.contains("Beta Agent v2", regex=False)
152
- ].iloc[0]
153
- assert beta["submission_name"] == "[Beta Agent v2](/reports/sub-b.html)"
154
-
155
-
156
- def test_legacy_row_submission_name_is_plain_text(monkeypatch):
157
- """Rows without submission_sha256 (legacy seeds) keep plain-text names.
158
-
159
- No `reports/<id>.html` exists on the dataset for those rows, so
160
- we don't wrap the name in a link.
161
- """
162
- monkeypatch.setattr(leaderboard, "_load_rows_from_hub", lambda: _stub_rows())
163
- _, unvalidated = leaderboard.load_leaderboard_split()
164
  gamma = unvalidated[unvalidated["submission_name"] == "Gamma baseline"].iloc[0]
165
- # Plain text: no `[...](...)` wrapping.
166
- assert "[" not in gamma["submission_name"]
167
- assert "](" not in gamma["submission_name"]
168
- # And the hidden report_url column is empty for the same reason.
169
  assert gamma["report_url"] == ""
170
 
171
 
 
133
  assert list(unvalidated.columns) == leaderboard.LEADERBOARD_COLS
134
 
135
 
136
+ def test_submission_name_is_plain_text(monkeypatch):
137
+ """`submission_name` cells render as plain text on both tables.
138
+
139
+ Earlier iterations wrapped the name as a markdown link to the
140
+ report proxy, but the proxy URL 404s for logged-in users on a
141
+ private Space (no auth carryover on same-origin pathname
142
+ navigations). Reports are rendered inline via an iframe viewer
143
+ on row-click instead, so the name cell stays plain text.
144
  """
145
  monkeypatch.setattr(leaderboard, "_load_rows_from_hub", lambda: _stub_rows())
146
  validated, unvalidated = leaderboard.load_leaderboard_split()
147
+ for name in [
148
+ "Alpha Agent v1",
149
+ "Beta Agent v2",
150
+ "Gamma baseline",
151
+ ]:
152
+ present = (
153
+ (validated["submission_name"] == name).any()
154
+ or (unvalidated["submission_name"] == name).any()
155
+ )
156
+ assert present, f"{name!r} should be present as a plain-text cell"
157
+ # The hidden `report_url` column is still computed (used by the
158
+ # row-click handler to decide whether to embed the report iframe).
159
  alpha = validated.iloc[0]
160
+ assert alpha["report_url"] == "/reports/sub-a.html"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  gamma = unvalidated[unvalidated["submission_name"] == "Gamma baseline"].iloc[0]
 
 
 
 
162
  assert gamma["report_url"] == ""
163
 
164
 
tests/test_proxy.py CHANGED
@@ -1,14 +1,28 @@
1
- """Unit tests for the per-submission report proxy route.
2
-
3
- The Space exposes ``/reports/{submission_id}.html`` which fetches the
4
- file from the submissions dataset and re-serves it as
5
- ``Content-Type: text/html`` (the dataset's ``/resolve/`` returns it
6
- as ``text/plain``, which makes the browser show source). Tests cover
7
- the handler's response shape with the fetch monkeypatched out, so
8
- the suite has zero network I/O.
 
 
 
 
 
 
 
 
 
9
  """
10
  from __future__ import annotations
11
 
 
 
 
 
 
12
  import app
13
 
14
 
@@ -58,3 +72,112 @@ def test_proxy_route_is_registered():
58
  """
59
  routes = [getattr(r, "path", None) for r in app.app.routes]
60
  assert "/reports/{submission_id}.html" in routes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Unit tests for the report-proxy route and the inline iframe viewer.
2
+
3
+ The Space exposes two paths for the per-submission HTML report:
4
+
5
+ - ``/reports/{submission_id}.html`` (FastAPI route): re-serves the
6
+ file with ``Content-Type: text/html``. Works under Bearer auth
7
+ for programmatic clients; gets 404'd by HF's edge for logged-in
8
+ browser users on a private Space (no auth carryover across
9
+ same-origin pathname navigations). Kept as a backdoor / for the
10
+ future public migration.
11
+ - ``_format_detail_and_report`` (row-click handler): server-side
12
+ fetches the report via ``hf_hub_download`` and inlines it into
13
+ an ``<iframe srcdoc=...>``. No browser HTTP request → no edge
14
+ auth gate → renders for any logged-in user.
15
+
16
+ Tests stub the Hub fetch via monkeypatch so the suite has zero
17
+ network I/O.
18
  """
19
  from __future__ import annotations
20
 
21
+ import re
22
+ import types
23
+
24
+ import pandas as pd
25
+
26
  import app
27
 
28
 
 
72
  """
73
  routes = [getattr(r, "path", None) for r in app.app.routes]
74
  assert "/reports/{submission_id}.html" in routes
75
+
76
+
77
+ # --- Inline iframe viewer (_format_detail_and_report) ----------------
78
+
79
+ def _stub_row(**overrides):
80
+ base = {
81
+ "submission_id": "sub-test-x",
82
+ "submission_name": "Test Agent",
83
+ "submitter_name": "team-test",
84
+ "status": "completed",
85
+ "submitted_at": "2026-05-26T12:02:31Z",
86
+ "notes": None,
87
+ "model details (optional)": "_None_",
88
+ "submission_blob_url": "https://example.test/sub-test-x.zip",
89
+ "report_url": "/reports/sub-test-x.html",
90
+ "failure_reason": None,
91
+ }
92
+ base.update(overrides)
93
+ return pd.DataFrame([base])
94
+
95
+
96
+ def _fake_evt(idx=0):
97
+ """Minimal stand-in for gr.SelectData with the .index attr we read."""
98
+ return types.SimpleNamespace(index=[idx, 0])
99
+
100
+
101
+ def test_iframe_viewer_inlines_report_for_modern_row(monkeypatch):
102
+ """A completed modern row's HTML lands inside <iframe srcdoc>.
103
+
104
+ Confirms the fetched bytes are HTML-escaped (so `<html>` is not
105
+ re-parsed by the host page) and the iframe carries explicit
106
+ sizing so Gradio's column flex can't clip it vertically.
107
+ """
108
+ monkeypatch.setattr(
109
+ app, "_fetch_report_html",
110
+ lambda sid: b"<!DOCTYPE html><body><h1>Report for " + sid.encode() + b"</h1></body>",
111
+ )
112
+ df = _stub_row()
113
+ md, iframe = app._format_detail_and_report(df, _fake_evt())
114
+ assert "### Test Agent" in md
115
+ assert iframe.startswith('<iframe srcdoc="')
116
+ # HTML-escaped content: literal "<!DOCTYPE html>" becomes
117
+ # "&lt;!DOCTYPE html&gt;" inside the attribute.
118
+ assert "&lt;!DOCTYPE html&gt;" in iframe
119
+ assert "sub-test-x" in iframe
120
+ assert 'style="width:100%; height:90vh; border:0; display:block;"' in iframe
121
+
122
+
123
+ def test_iframe_viewer_empty_for_pending_or_failed_row(monkeypatch):
124
+ """Rows without a report_url get an empty viewer (no iframe at all).
125
+
126
+ Pending: still evaluating; no report exists yet. Failed: eval
127
+ crashed; no report uploaded. Legacy: pre-modern-pipeline; the
128
+ file genuinely doesn't exist on the dataset. All three are
129
+ handled by the same `report_url == ""` gate.
130
+ """
131
+ monkeypatch.setattr(app, "_fetch_report_html", lambda sid: b"unused")
132
+ for row_overrides in [
133
+ {"status": "pending", "report_url": ""},
134
+ {"status": "failed", "report_url": "", "failure_reason": "boom"},
135
+ {"status": "completed", "report_url": ""}, # legacy
136
+ ]:
137
+ df = _stub_row(**row_overrides)
138
+ md, iframe = app._format_detail_and_report(df, _fake_evt())
139
+ assert iframe == "", f"expected empty viewer for {row_overrides}"
140
+ # The metadata panel still renders.
141
+ assert "### Test Agent" in md
142
+
143
+
144
+ def test_iframe_viewer_falls_back_to_empty_when_fetch_fails(monkeypatch):
145
+ """If _fetch_report_html returns None (Hub blip), no iframe is emitted.
146
+
147
+ Avoids surfacing a broken iframe on a transient failure.
148
+ """
149
+ monkeypatch.setattr(app, "_fetch_report_html", lambda sid: None)
150
+ df = _stub_row()
151
+ _md, iframe = app._format_detail_and_report(df, _fake_evt())
152
+ assert iframe == ""
153
+
154
+
155
+ def test_iframe_viewer_returns_placeholder_on_null_event():
156
+ """A null SelectData (no row clicked) returns placeholder + empty viewer."""
157
+ df = _stub_row()
158
+ fake = types.SimpleNamespace(index=None)
159
+ md, iframe = app._format_detail_and_report(df, fake)
160
+ assert md == app.DETAIL_PLACEHOLDER
161
+ assert iframe == ""
162
+
163
+
164
+ def test_iframe_escape_is_attribute_safe(monkeypatch):
165
+ """Quotes / ampersands inside the report HTML are escaped properly.
166
+
167
+ A `"` inside the report would otherwise terminate the srcdoc
168
+ attribute prematurely and break parsing. Regression guard.
169
+ """
170
+ monkeypatch.setattr(
171
+ app, "_fetch_report_html",
172
+ lambda sid: b'<html><body>tag: <a href="https://x.test">x</a> & co.</body></html>',
173
+ )
174
+ df = _stub_row()
175
+ _md, iframe = app._format_detail_and_report(df, _fake_evt())
176
+ # Within the srcdoc value, double-quotes must be HTML-escaped.
177
+ srcdoc = re.search(r'srcdoc="(.*)"\s+style=', iframe, re.DOTALL)
178
+ assert srcdoc is not None
179
+ inner = srcdoc.group(1)
180
+ # No unescaped " inside the attribute value.
181
+ assert '"' not in inner
182
+ assert "&quot;" in inner
183
+ assert "&amp;" in inner