[v2 Substrate] Notebook artifact preview — iframe-isolated, dark-mode, canonical-by-ID fallback

← All Specs

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)

    File: notebook-preview-embed-v2_spec.md
    Modified: 2026-05-20 18:04
    Size: 4.8 KB