Effort: standard
Background
v1 shipped three sequential fixes for the same notebook-preview
artifact page over the course of ~2 hours on 2026-05-10:
PR #1381 added a third fallback to artifact_detail's
notebook branch — probe
data/scidex-artifacts/notebooks/<id>.html by canonical ID
when both
artifacts.metadata.rendered_html_path and
notebooks.rendered_html_path are NULL (the v1 polymorphic
backfill never propagated those pointers). Same PR backfilled
877 artifact metadata rows + 573 notebook rows.
PR #1382 switched the inline embed (a <div> containing a
truncated
<!DOCTYPE html>...<html> document — invalid nested
HTML that browsers rendered as a blank box) to an
<iframe>.
Same PR removed
/notebook/{id} from the type-alias 301
decorator stack at
api.py:34565 — it had been shadowing the
dedicated
notebook_detail handler, causing every "Open Full
Notebook" link to redirect back to the artifact page it was on.
PR #1384 added GET /api/notebooks/{id}/embed returning
the rendered Jupyter HTML run through
_darkify_notebook() wrapped in a SciDEX-themed standalone document. The artifact-
page iframe now sources from this endpoint so the preview
matches the dark theme.
The session also documented this pattern as the second occurrence
of the type-alias 301-shadowing trap (memory entry
uuid_migration_alias_route_traps.md).
Goal
Encode the v1 hard-won preview behavior as required behavior for
v2 substrate's notebook artifact viewer, so we don't relearn
these three bugs in v2.
Acceptance Criteria
☐ v2's notebook artifact viewer **never inlines the full
rendered Jupyter HTML** into the artifact page DOM. The
preview is always an
<iframe> sourced from a substrate
endpoint.
☐ v2 has a GET /api/notebooks/{artifact_id}/embed (or
whatever the v2 path turns out to be) that returns the
rendered notebook content **darkified and wrapped in a
standalone HTML document**. The iframe sources from here
so the embed matches the SciDEX page theme.
☐ v2's substrate file resolver locates the rendered HTML via
three fallbacks, in order: explicit metadata pointer →
DB join-table pointer → canonical-by-ID filesystem probe
under whatever v2's artifact-storage root is. The lesson
from v1 was that pointer rows go stale during backfills
while the file remains in canonical position.
☐ v2's route table has **exactly one canonical route per
resource type**. No type-alias 301 redirects that can
shadow dedicated handlers in registration order. If
legacy URLs need to be supported, they live in a
dedicated redirect map evaluated after the canonical
handlers, not as decorator-stack aliases.
☐ A test asserts that
/api/notebooks/<id>/embed returns content containing
both the SciDEX dark-theme CSS variables AND the notebook's
cell content — protects against the v1 truncation bug
where 200 KB of nbconvert head/CSS was returned with zero
cell HTML.
Approach
Iframe + embed endpoint. Mirror v1's PR #1382 + #1384
structure. The substrate version of
_darkify_notebook should
re-wrap the body in a substrate-themed standalone document
rather than emitting a fragment.
Resolver fallback chain. Substrate's artifact resolver
should accept a "find rendered" mode that returns the local
path of a rendered HTML if one exists, falling back through
the three sources listed above. v1's
_find_notebook_rendered_html in
api.py is a reference
implementation.
Route hygiene. When defining substrate FastAPI routes,
avoid stacking multiple
@app.get decorators on a redirect
handler that competes with a dedicated handler later in the
file. Either:
- Register the dedicated handler first and add the alias as
a
@app.get(..., name='legacy_alias') after it (FastAPI
is order-sensitive — first match wins), OR
- Avoid alias routes entirely and use a single redirect map
middleware evaluated after the router.
Dependencies
- v2 substrate's artifact viewer route (whichever module owns
the artifact detail page in substrate)
- v2 substrate's notebook artifact storage layout (the
fallback chain only works if files are addressable by
canonical ID)
Non-Goals
- Backporting any of this to v1. v1 ships the working
implementation already; the freeze locks it.
- Regenerating the 515 v1 notebooks identified as missing
rendered HTML on disk. That's a v1 data gap; v2 substrate
starts with its own rendered-content guarantees.
Work Log
(empty — to be filled in by the substrate implementer)