Skip to content

Wayfinder: close out the SUBSIDE UI SOW — TACC signs off on ui-react at mint.tacc.utexas.edu #79

Description

@mosoriob

Destination

TACC uses ui-react at mint.tacc.utexas.edu in place of the Lit UI, and the client
manager signs off that the SOW's acceptance criterion (§8 — a model input's variable,
standard variable and unit configured in one form submission) is met.

That sign-off closes out the SOW. The map ends there.

Notes

Domain. MINT / SUBSIDE model catalog. See ADR-0002
for the React migration and ADR-0001
for the Hasura backend it sits on. Both have a Traps section — read it before debugging
anything that looks like a permissions or cache bug.

This map carries execution, not just decisions. The remaining work is deploy-and-verify,
so tickets do the thing rather than only decide it.

Contract. The SUBSIDE model-catalog UI SOW with UT Austin / TACC. Its period of
performance has expired (confirmed not to be a blocker) and the engagement is settled
financially, so the only remaining obligation is delivery and sign-off.

Contract number, terms, amounts, billing status and client contacts are deliberately not
recorded here
— this repo is public. They live in the private metalearn wiki. Keep them
out of this map and its tickets.

Standing preferences for this effort

  • Deploy target is TACC's own small k8s cluster, via the MINT Helm chart. Max has kubectl access.
  • ui-react replaces Lit at mint.tacc.utexas.edu (decided; T1 de-risks it).
  • No UAT walkthrough document — TACC gets the URL (decided).
  • Skills: /grilling and /domain-modeling on decision tickets.

State at charting (2026-08-08). Last code commit 2026-07-20; ~3 weeks idle. ui-react is
containerized and published, 21 routes ported, and has run only on Vercel previews and
localhost. OAuth2 has been exercised on previews and works — but previews use a dedicated
OAuth path for preview origins, so the round-trip at mint.tacc.utexas.edu is still unproven.
The app has never been driven against TACC's own Hasura and CKAN.

Decisions so far

  • Confirm SUBSIDE can live without the un-ported Lit screens
    none are needed; the flip is free. Most are already dead or stubbed in Lit, Analysis and
    Emulators have no nav link, thread Summary turned out to be ported, and thread Visualize was
    never functional in Lit either. Also settled the rollout: no parallel deploy (no users on
    Lit at TACC), Vercel is the test surface, and Lit stays deployed-but-unrouted at the upgrade
    as the only undo, removed after sign-off.

  • Tag helm-charts 9.0.0-beta.3 and bump the submodule pointer off beta.2
    the chart is reachable again. 9.0.0-beta.3 tagged upstream on 3d3aca8, submodule bumped
    in #82, merged to develop as 200a0d7.
    Both confirmations held: ui_react.enabled: false by default, and the legacy ui
    component is untouched (values.yaml is +52/-0, no ui* template in the diff, _helpers.tpl
    append-only). Three facts fell out — ui_react needs an OAuth2 client distinct from
    Lit's (one callback URL per client, so reusing Lit's breaks the undo — sharpened
    #73); its image tag is a monorepo SHA
    the bump automation will silently skip, so it is hand-set; and it takes its own host
    rather than a path prefix, making the flip a host reassignment.

  • Verify CKAN from a browser origin against ckan.tacc.utexas.edu — is CORS actually broken?
    fine in production, broken on Vercel. CKAN echoes Access-Control-Allow-Origin only for
    *.tacc.utexas.edu (not the apex), localhost and 127.0.0.1; every other CORS header is
    sent to everyone, so it looks enabled when it is not. mint.tacc.utexas.edu is allowlisted,
    so production needs nothing from TACC and nothing from us — which killed
    #74 (the nginx proxy only serves the
    origin that already works, and never touches a preview), closed unopened. *.vercel.app got
    no header — since fixed: Max added the two real Vercel origins to CKAN's nginx map as
    anchored regexes and dropped the pre-existing *.github.io wildcard, verified live on every
    action the app calls plus the preflight. So
    #76 runs wholly on the Vercel preview as
    originally written, /datasets/* included, on the branch alias
    monorepo-git-develop-mosoriobs-projects.vercel.app — per-deployment preview URLs are refused
    and cannot be allowlisted, since the hash changes every push. No public-suffix wildcard is left
    in the allowlist, which closes the credentialed-access concern the first fix opened.

  • Commit .env.example and local-setup notes so ui-react runs for someone who is not Max
    a .env file cannot configure the dev server. That was the finding, and it made the
    ticket's premise wrong. index.html always loads public/env-config.js, so the VITE_*
    fallbacks never run. The real fault was one line: HASURA_ENDPOINT pointed inside the
    cluster, at graphql.mint.local. It now points to TACC's public Hasura. A clone plus
    npm install plus npm run dev now shows real model catalog data, read-only, with no
    cluster access. I verified this live from a localhost:3000 origin. The ticket also added
    .env.example, npm run config:local and a README section, and deleted
    .env.development. It deleted MODEL_CATALOG_API, which no code used. That left
    #75 with a fixed list of 12 keys.
    ENSEMBLE_MANAGER_API must be set, or thread execution stops without an error. The code is
    in PR #83, merged to develop as
    45a6d7e
    .

  • Check whether the OAuth2 redirect URI for mint.tacc.utexas.edu is already registered
    already registered; nothing to do at the IdP. The premise was wrong: Lit uses
    /oauth2/callback too (${window.location.origin}/oauth2/callback, hardcoded), so both apps
    want the identical redirect URI. The IdP is Tapis, tenant portals — not Keycloak — and
    client mint has callback_url exactly https://mint.tacc.utexas.edu/oauth2/callback.
    Verified live, with negative controls, against GET /v3/oauth2/authorize, which validates
    redirect_uri before login: the match is exact on scheme, host and path (http:// is
    rejected), and no mint-*react* client exists. Tapis really is one-callback-per-client, but
    the Tag helm-charts 9.0.0-beta.3 and bump the submodule pointer off beta.2 #70 inference that ui_react needs its own client does not follow — the two apps want
    the same URI and never serve the origin at once (Lit is unrouted). Decision: reuse mint,
    so there is no IdP change at flip or rollback. The chart's own text argues the opposite and
    is wrong for this rollout; it only hard-fails on an empty client_id, so "mint" renders —
    flagged on #75. Two constraints landed on
    #77: the host must be exactly
    https://mint.tacc.utexas.edu, and Lit must be unrouted, never re-hosted — parked at a
    second hostname it cannot log in, making it a rollback to a broken app. Also rules out a
    staging host for ui-react before the flip.

  • Settle the window.MINT_CONFIG values for the TACC deployment
    two values, not twelve. The deployed Lit app publishes TACC's real config at
    mint.tacc.utexas.edu/config.js, so no value had to be guessed. Ten of the twelve keys need
    no statement: they derive from the component ingresses or inherit shared chart values TACC
    already overrides (google.maps.key, welcome_message). Only client_id: "mint" and
    data_catalog_api: "https://ckan.tacc.utexas.edu" must be set. Derivation is proven safe
    for Hasura and the Ensemble Manager rather than assumed — the Lit template and the ui_react
    helper share the same ingress.tls idiom, and Lit's live output shows https:// for both, so
    TACC declares TLS and there is no mixed-content trap. CKAN is the exception: ui_react derives
    it from components.data_catalog, which TACC does not use (it uses external_services.ckan.url,
    read by the Lit template only), so the key is omitted and the app falls back to a compiled
    default that is right by coincidence — hence the override. Verified live from the production
    origin: Hasura and the Ensemble Manager both send Access-Control-Allow-Origin: *, the
    Ensemble Manager's /v1 routes exist, and CKAN sets no X-Frame-Options, so the datasets
    iframe will render. Two facts landed on
    #76ui-react needs no CKAN API key
    (Lit's DATA_CATALOG_KEY has no counterpart; all reads are anonymous GETs), and
    ENSEMBLE_MANAGER_API is now set, so thread execution is in scope for the smoke test. Three
    chart cleanups landed on #77: the dead
    MODEL_CATALOG_API key, the wrong "must be a distinct client" text, and teaching
    ui-react-config.yaml to read external_services.ckan.url. None block the flip.

  • Ensemble Manager calls send no Authorization header — localStorage key mismatch
    fixed in PR #87, merged to develop as
    4f7debf
    .
    Both REST calls read the token by string literal from a key nothing writes, and spread the
    header conditionally, so every call went out anonymous with no error. The symptom is now known
    rather than guessed, from the Ensemble Manager source: against a tapis engine, which is
    what TACC runs, submit returns 500 ("Unauthorized" escapes as a plain Error) and logs
    return 401 — both read as server faults, not a missing credential. The probe could not
    settle this because fetchLog looks the execution up before it reads the header, so an
    unknown id 404s either way. Both calls moved into src/lib/ensemble-manager.ts so the header
    is built once; the tests assert the outgoing header, not the storage key, and were checked
    to fail against the old code. The fix exposed a second defect and did not resolve it:
    #88ui-react posts to
    /executionEngines/localex, and the only such route is /executionEngines/tapis, so run
    submission 404s before auth matters. Submission is still broken at TACC; the log call is
    fully fixed.

  • ui-react submits runs to /executionEngines/localex, a route that does not exist
    two faults, not one. The engine name was wrong for TACC, and the route was wrong for every
    engine except Tapis: server.ts mounts /executionsLocal, /executions and
    /executionEngines as three separate routers, and the last registers /tapis only. So a
    config key alone would not have fixed it. The engine is now the config key
    EXECUTION_ENGINE, and executionEnginePath maps each backend to its real route — the same
    map Lit makes in mint-runs.ts:731 — so the key is honest for all three engines. The
    default is localex
    , matching both the chart default for the Ensemble Manager and the Lit
    fallback, so one unset value describes the same deployment in three places. The results step
    needed nothing: MintResults reads the engine off the execution row, not from config, and its
    localex/wings rewrite is dead because MintThread never passes mintConfig. Code is in
    PR #89, merged to develop as 1d1ada9.
    One constraint landed on #77: the chart must
    emit EXECUTION_ENGINE: tapis for TACC, derived the way ui-config.yaml derives Lit's — a
    prerequisite for run submission, not a cleanup. Both halves confirmed live on
    #76:
    TACC's Ensemble Manager publishes
    exactly the three separate mounts predicted here, and its Lit config sets
    REACT_APP_EXECUTION_ENGINE = "tapis".

  • Smoke-test ui-react against TACC's real Hasura and CKAN
    the §8 acceptance criterion passes, for a new input. Signed in as mosorio@portals, a new
    model input took label + standard variable (soil__porosity) + unit (m3 m-3) in one
    submission against TACC's live catalog. The form is genuinely flattened — one control for
    standard variable and unit, which offers the units already used with the variable chosen. The
    cmdk trap ADR-0002 warns about could not apply: this picker is plain buttons, not cmdk. Verified
    anyway by hit test and a real mouse click. Run on localhost:3000, not the Vercel preview
    the preview is behind Vercel SSO, and localhost is CKAN-allowlisted with a working OAuth
    client, so it tests strictly more. Editing an existing input fails, and that is
    #91, now blocking
    #77: the junction upsert names a column this
    repo's own metadata never exposes, so it is not TACC-specific, and it half-applies. A second,
    minor fault rides along — a successful save renders stale until a manual reload. Thread
    execution was carved out as #92 rather than
    fired at the client's cluster unasked. /modeling/* renders against real data; note the region
    filter defaults to California, where TACC has nothing — it reads as an empty deployment until
    you switch to Texas. All test rows were removed; TACC's catalog is back to its prior state.

  • Editing an existing model input fails: the junction upsert names a column Hasura does not expose
    fixed and verified live; the write surface is a graphql_engine question, not a TACC one.
    Client-only, so nothing to apply to TACC's Hasura. SetConfigurationInputOptional replaces the
    upsert with delete_by_pk + insert_one in one document — Hasura runs a mutation's root
    fields in a single transaction, so the row is never left missing — and it now fires only when
    is_optional changed
    , instead of for every row in toUpdate. Proven under a real Tapis token
    with a zero-write probe: validation precedes execution, so the old document returns
    validation-failed (the exact _PLACEHOLDER error) and the new one reaches execution. Why
    nothing caught it:
    generated/schema.graphql is the admin schema, where the column is
    updatable — codegen and all 760 offline tests validate against a schema the app never runs as.
    Treat any role-sensitive write as unverified until it runs under a real token. Fault 2 fixed too:
    the form refetches GetConfiguration before onSaved, so a save no longer renders stale.
    End-to-end on simpleModelAnnotated — add, edit label, toggle Optional — all clean, and the
    catalog was returned to its exact prior state. Code in
    PR #93. One thing deliberately not fixed:
    the save is still many mutations, not one transaction, so a mid-sequence failure still
    half-applies. No step on this path can fail that way now; ticket it if TACC hits it.

  • Thread datasets: ui-react searches CKAN free text instead of the mint_standard_variables field
    fixed and verified live: 3 of 40 annotated variables → 40 of 40. Client-only; nothing to
    change at TACC. findDatasets no longer sends a q at all — packagesMatchingVariables keeps
    the packages whose resources carry the variable, as Lit does. The ticket's premise was
    re-measured first and held. Three defects the ticket did not name, each real: resource
    narrowing was mandatory, not cosmetic — a dataset matches because some of its resources carry
    the variable, and one TACC package holds 35 resources, 1 of which does, so binding all of
    them handed the model input files it cannot read; datatype was read off the first resource,
    which MintDatasets then filters the list on; and rows=1000 was a silent cap, now paged.
    Two existing fixtures encoded the bug — they carried no annotation and passed only because
    the old code matched prose. Code in PR #95,
    CI green. Submit one thread run end to end against TACC's Ensemble Manager #92 is unblocked, with one constraint landed on it: ext_bbox drops any package
    with no spatial extent, and 11 of TACC's 33 annotated packages have none — Texas keeps 16 of
    33, the California default keeps 0. Lit behaves the same, so it is not a regression, but a
    region-scoped thread cannot see a third of the annotated catalog — now
    #97
    . The same root cause survives
    on the browse surface as #96, deliberately
    not fixed here.

  • Missing dataset metadata must not mean an invisible dataset (decided 2026-08-09, no ticket
    of its own to close — it scopes #97). The
    four constraints on the Datasets step disagreed about what unknown means: the standard
    variable drops it (right), the datatype offers a "Show N" link, dates keep it, and the
    region dropped it silently — because ext_bbox filters on having a location, not on
    location. Region was also the only constraint on the server. The rule is now two rules:
    unknown extent is shown and badged, as dates already do; known-but-outside is hidden behind the
    counted link the datatype filter already has. Chosen over a single show-everything toggle,
    which cannot say why a row was hidden, and over silently widening the results — the exact
    failure Thread datasets: ui-react searches CKAN free text instead of the mint_standard_variables field #94 had just demonstrated. Cheap only because Thread datasets: ui-react searches CKAN free text instead of the mint_standard_variables field #94 made findDatasets read the whole
    catalog, so dropping ext_bbox costs no request. One trap found while scoping it:
    packageSpatialCoverage reads coordinates[0] and handles a bare Polygon only, while TACC also
    holds FeatureCollection ×5, Feature, Point and MultiPolygon — moving the filter
    client-side without a real bbox extractor would drop those 8 without a word. Datasets step: show the datasets the region filter hides, instead of dropping them #97 blocks
    #77
    : TACC must meet a correct Datasets
    step, not an empty one. One thing Datasets step: show the datasets the region filter hides, instead of dropping them #97 does not yet say, seen live on Submit one thread run end to end against TACC's Ensemble Manager #92: Thread datasets: ui-react searches CKAN free text instead of the mint_standard_variables field #94 made
    findDatasets read the whole catalog, so the region now narrows the Datasets step not at
    all
    — an Alaska package (Bethel Elevation) was offered under a Texas framing. That is the
    opposite failure to the one Datasets step: show the datasets the region filter hides, instead of dropping them #97 was written for, and the same rule has to settle both.

  • Dataset search by variable name searches CKAN free text, not mint_standard_variables
    fixed and verified live: groundwater 48 → 12, corpus_nlp 0 → 17. Client-only; nothing to
    change at TACC. The measured premise held exactly. Substring-match the annotation, prose
    dropped
    — chosen over keep-prose-and-badge, which leaves the count wrong and cannot fix
    corpus_nlp at all (Solr splits the name on _ before it matches), and over a typeahead on
    Hasura's 668 standard variables, where only a small fraction are carried by any TACC dataset, so
    a user picks a legitimate variable and gets a confident 0. Substring matching only ever offers
    what exists, and it agrees with the Datasets step: show the datasets the region filter hides, instead of dropping them #97 rule: filtering by standard variable correctly drops a
    dataset that carries no annotation. A second, independent fault rode along and hit both modes:
    searchDatasets took CKAN's rows=100 default while TACC holds 215 packages, so the empty term
    listed 100 of 215 and said nothing — now searchAllPackages, which Thread datasets: ui-react searches CKAN free text instead of the mint_standard_variables field #94 had already taught to
    page. buildSearchQuery lost its variables branch entirely; it only ever built a query Solr
    cannot answer, and this was the second surface it broke. One ticket framing was wrong: this was
    never a regression.
    Lit's own /datasets/search is dead at TACC — it POSTs to
    data_catalog_api + "/find_datasets" (ui/src/screens/datasets/actions.ts:377), the legacy MINT
    Data Catalog route, never routed through DataCatalogAdapter, so data_catalog_type = "CKAN"
    does not redirect it; TACC answers 405 for both modes. So "match Lit" was not available as a
    constraint. Code in PR #100, full suite green
    at 813 tests; the 22 new assertions were checked to fail against the old code, on a path that had
    no test file at all.

  • Thread wizard shows 18 of 175 model configurations: the model tree query accepts only sdm#Model
    no type belongs out of the model tree; all three queries drop the predicate. Verified live in
    the signed-in app: the Models step now reports 175 where it reported 18, and HAND — TACC's
    largest family, 44 configurations — goes 0 → 44. The ticket's premise was wrong. type
    classifies a model; it does not say whether a row is one. The ETL only migrated
    ?id a sdm:Model and stored the most specific subtype, so every row is a model by construction,
    and the 8 sd#Software rows carry the ontology superclass — MODFLOW ×5, ParFlow, an InSAR
    workflow, a particle filter. So _neq sd#Software, the ticket's own candidate, would have hidden
    real models. An allowlist is worse and this was measured, not argued: model-catalog-api
    keeps one and it is already stale, serving 44 of 55 — #102,
    ruled out of scope below. All three sub-questions answered the same way: GetModelFamilies too
    (registration could not offer any subtype family, so no version could be registered under HAND),
    and RegionModels too (it shares the query, so a different rule would only make them disagree).
    Registration still writes sdm#Model for a new family, which is correct — read and write are
    different questions. Code in PR #101, 802
    tests green, the 9 new assertions checked to fail against the old code; graphql.ts regenerated
    by codegen from the committed admin-schema snapshot rather than hand-edited, which is worth
    remembering — codegen runs offline against generated/schema.graphql, no admin secret needed.
    Submit one thread run end to end against TACC's Ensemble Manager #92 is unblocked. The fix also exposed the next wall in the same step, now
    #103: the Variables step offers 668
    indicators where 147 can find a model.

  • Thread execution blocks the flip; ui-react must run a thread before TACC gets the URL
    (decided 2026-08-09 by Max, no ticket of its own to close — it scopes
    #104). The SOW's §8 criterion is already
    met (#76), so Thread wizard dead-ends at Parameters: threadExecutionData is never loaded #104 is not required by the
    contract's letter. It is required anyway: Lit runs threads at TACC today, and flipping the host
    to an app that cannot would hand the client a regression, whatever §8 says. Chosen over shipping
    on §8 alone, and over asking the client — asking would invite a "no" to a question we would not
    act on. Consequence: Thread wizard dead-ends at Parameters: threadExecutionData is never loaded #104 is now the largest item left on the map, and unlike Editing an existing model input fails: the junction upsert names a column Hasura does not expose #91/Thread datasets: ui-react searches CKAN free text instead of the mint_standard_variables field #94/Dataset search by variable name searches CKAN free text, not mint_standard_variables #96/Thread wizard shows 18 of 175 model configurations: the model tree query accepts only sdm#Model #98
    it is a data path to build, not a query to correct. Sequence it before
    #97,
    #99 and
    #103, which are all smaller.

  • Thread wizard dead-ends at Parameters: threadExecutionData is never loaded
    the pipeline now loads from Hasura, and the ticket's premise was half the fault.
    threadExecutionData was useState(null) that nothing loaded, as written — but
    DatasetsStep persisted nothing either, calling UpdateThreadData with empty data
    and modelIO against a mutation that deletes every binding first. So Continue destroyed
    bindings, and Thread wizard dead-ends at Parameters: threadExecutionData is never loaded #104's own measurement that the assignment survived a reload is retracted.
    That mattered: loading the pipeline alone would have changed nothing, because there was
    never a binding to read back. GetThreadExecution + threadExecutionFromGQL are ported
    from Lit's get.graphql and graphql_adapter.ts; Datasets and Parameters now write;
    executions load and the summary polls while runs are in flight. Three defects found in
    the wiring, all where the React port had drifted from Lit:
    resource ids were a djb2 hash
    where Lit writes MD5 of the URL — and since this ticket is what starts writing those rows,
    shipping it would have duplicated every file TACC already had; run_progress is a
    fraction, drawn as a percentage, so a finished run rendered as a 1% sliver; and total_runs
    dropped the input-resource factor, where TACC's own summaries keep it. Verified live three
    ways — anonymously for the whole thread_models subtree and the executions query, under a
    real Tapis token for thread.thread_data and dataslice.dataset (both user-only), and a
    zero-write probe of the parameters mutation that reached constraint-violation rather than
    validation-failed. The running app renders Runs ✓ Complete from TACC where every step
    read Pending before. Code in PR #105,
    852 tests green. generated/schema.graphql is the admin schema — the role trap Editing an existing model input fails: the junction upsert names a column Hasura does not expose #91
    found for writes applies to reads too
    : anonymous sees neither thread_data, nor
    thread.events/permissions, nor the dataset table. Submit one thread run end to end against TACC's Ensemble Manager #92 is unblocked, and carries
    the one thing Thread wizard dead-ends at Parameters: threadExecutionData is never loaded #104 could not prove: the two write round-trips through the UI.

  • Submit one thread run end to end against TACC's Ensemble Manager
    a run went from ui-react to TACC's cluster and finished. Model SUBSIDE Allocation Specification, POST /v1/executionEngines/tapis202, two real Tapis jobs, one of
    which ran to completion and wrote a full output set. That is the first ui-react run to
    produce output at TACC, and it retires the "unproven path" risk this ticket existed for.
    Of the four checks, two pass and two fail. Submit returns 2xx, and the Runs step polls
    and tracks status live with no 401 — so Ensemble Manager calls send no Authorization header — localStorage key mismatch #85 and ui-react submits runs to /executionEngines/localex, a route that does not exist #88 both hold under a real submission.
    The execution row says execution_engine: localex
    (#109): a hardcoded literal in
    ExecutionCreation, harmless today because the server reads prefs.execution_engine
    instead, wrong for every consumer of the column. And the Results step never shows any
    output
    (#110) — two independent
    causes, which matters because fixing either alone changes nothing: MintResults guards on
    onPublishResults, a prop MintThread never passes, so Fetch results makes no request at
    all
    ; and the route it should call returns 500 wrapping a 403 from Tapis or CKAN. Results step never shows any output: the Fetch results button is not wired, and the publish route returns 500 #110
    blocks Flip mint.tacc.utexas.edu to ui-react #77. The wall-per-fix pattern has finally run out of walls in this half — the
    wizard now walks Framing → Results without a new blocker appearing, and the two failures
    are at the far end, not one step in. Two workarounds confirmed as sound: Models step Continue does nothing once a thread has execution data: delete_thread_model is refused, and the error is swallowed #107 only bites
    a thread that already holds bindings, so a fresh thread clears the Models step, and Variables step cannot save any indicator: it writes a standard-variable URI into a column that references the legacy variable table #106 is
    steered around by leaving the indicator empty. Both run failures were data, not code
    an .m4a fed to a JSON-reading model, then a CKAN resource holding invalid JSON (a raw
    newline at byte 25). Both were diagnosed from the Ensemble Manager log endpoint, which
    works: read the log before blaming the client.

  • Datasets step: show the datasets the region filter hides, instead of dropping them
    two faults, opposite in direction, and the ticket named only one. ext_bbox hid the 11 of
    33 annotated packages with no extent, as measured — but MintThread also never passed the
    region to DatasetsStep at all
    , so what did arrive was not narrowed either. One rule had to
    settle both, and does: unknown extent is shown and badged ! no location; known-but-outside is
    hidden behind a counted link. findDatasets now drops nothing for the region — it labels each
    dataset region_match: inside | outside | unknown and the UI decides. Verified live at TACC:
    Texas 29 offered (18 in-region + 11 badged) and 4 behind the link, where ext_bbox served
    16
    ; California — the default framing — 11 badged rows where it showed 0, which retires the
    "reads as an empty deployment" note. ext_bbox is not a server-side version of the same
    test:
    ckanext-spatial indexes bare geometries only, so it also drops a Feature or
    FeatureCollection, and TACC has two of those squarely inside Texas — hence 16 → 18, not
    16 → 16. The extractor replaced two partial readers, not one: the region map's
    calculateBoundingBox missed Feature.geometry and FeatureCollection.features too, so
    lib/geo/bbox.ts now serves both surfaces and the region picker gained the coverage for free.
    GetThread selects region { geometries }, confirmed readable under anonymous. Code in
    PR #113, 883 tests green; the
    Feature/FeatureCollection/Point assertions were checked to fail against both old readers.
    Two traps left standing, neither blocking: a bounding box is not the region — Alaska crosses
    the antimeridian, so its box spans nearly the whole globe and almost nothing reads as outside it
    (Lit is the same; it does not bite Texas) — and the browse surfaces still carry the old fault,
    now #114.

  • Deleting a problem statement, task or thread leaves the row behind and reports success
    the permission was unsatisfiable by construction, so this is the first fix on the map that
    needs a schema migration.
    The user role may delete one of these rows only while it still
    carries a CREATE provenance event; the provenance FKs were ON DELETE RESTRICT. Delete the
    provenance first and the row loses its own delete permission (0 rows, reported as success);
    delete the row first and Postgres refuses it. Both orders were run against a live database
    and neither works
    , which kills the ticket's own first option and rules out any client-only
    fix. The ticket named one failed root field; all eleven failed — every child on the tree is
    gated through the same provenance, so only the provenance and permission rows were ever really
    deleted, which is exactly the state Submit one thread run end to end against TACC's Ensemble Manager #92's orphan was found in. Six FKs become ON DELETE CASCADE in graphql_engine PR #14
    metadata unchanged, structural FKs untouched — and the client deletes bottom-up, never
    touching provenance, in PR #115 with
    assertDeleted to stop a zero-row delete rendering as success. A second defect had to be fixed
    with it: the port dropped Lit's dataslice/thread_data deletes, harmless while the
    mutation was a no-op but a hard FK error once Thread wizard dead-ends at Parameters: threadExecutionData is never loaded #104 started writing those rows. Verified on the
    migrated dev cluster under Hasura's own compiled predicates, in a rolled-back transaction; the
    Submit one thread run end to end against TACC's Ensemble Manager #92 orphan is cleared. Two things outlive this ticket. Lit's delete mutations are
    byte-identical, so delete is broken at TACC today — which is why Deleting a problem statement, task or thread leaves the row behind and reports success #99 no longer blocks
    #77
    (decided by Max, 2026-08-09, on the
    same rule Thread wizard dead-ends at Parameters: threadExecutionData is never loaded #104 set: a defect blocks the flip only if moving off Lit is a regression). And the
    chart's Hasura migration job is a post-install hook only, so helm upgrade never applies
    migrations — TACC needs the six-FK change run by hand, after the flip, preferably as a surgical
    run_sql rather than hasura migrate apply, whose state at TACC is unverified.

  • The indicator is a standard variable, and the Variables step does not block the flip
    (decided 2026-08-10 by Max, no ticket of its own to close — it scopes
    #106 and
    #103). Two calls, and the second one moves
    the critical path. Storage: thread.response_variable_id will hold a standard-variable URI.
    The four FKs on thread/task repoint from public.variable to
    modelcatalog_standard_variable, in a graphql_engine migration on the Deleting a problem statement, task or thread leaves the row behind and reports success #99 pattern — FKs change,
    metadata does not, because the relationships are foreign_key_constraint_on and follow the
    constraint. Chosen over writing the label, which covers 129 of the 147 producible output
    variables today and only decays: etl/load.py writes modelcatalog_* tables only, so
    public.variable has had no writer since the DYNAMO migration — treat it as frozen and build
    no list from it. Data cost at TACC is one row; the unmeasurable risk is ISI's, so the
    migration must map by label and null what does not map, never drop rows. Scope: Variables step cannot save any indicator: it writes a standard-variable URI into a column that references the legacy variable table #106 and Variables step offers 668 indicators; only 147 can find a model #103
    both stop blocking #77, on the same rule Deleting a problem statement, task or thread leaves the row behind and reports success #99
    set — Lit's Variables step is dead at TACC too, so moving off Lit loses nothing.
    ui/src/screens/variables/actions.ts:31 hardcodes is_indicator: false on every entry and
    renderResponseVariables filters on exactly that flag, so Lit's select renders zero options;
    live, 1 of 165 threads and 0 of 55 tasks carry a response variable and none carry a
    driving variable. ui-react's step is skippable, so it traps a user who picks but does not stop
    one who does not. Variables step cannot save any indicator: it writes a standard-variable URI into a column that references the legacy variable table #106 now blocks Variables step offers 668 indicators; only 147 can find a model #103, and Flip mint.tacc.utexas.edu to ui-react #77 is down to two blockers,
    #107 and
    #110
    two now one, Results step never shows any output: the Fetch results button is not wired, and the publish route returns 500 #110; Models step Continue does nothing once a thread has execution data: delete_thread_model is refused, and the error is swallowed #107 is
    fixed, see below. One consequence to file when the
    migration lands: the Ensemble Manager's POST /v1/problemStatements/*/subtasks validates these
    ids against variable, so it will reject every URI — pre-existing, and ui-react does not use that
    route.

  • Apply the modeling provenance cascade migration to TACC's Hasura
    delete works at TACC, and the migration alone was never enough. The schema change was applied
    on 2026-08-10 (six FKs read confdeltype = c, no drift, one 100-day idle in transaction session
    had to be terminated first — Set idle_in_transaction_session_timeout on TACC's Postgres #119, runbook docs: runbook for the TACC cascade migration #118). Step 4, the round trip under a real Tapis token,
    is now done: a throwaway problem statement created, deleted and gone after a reload, with
    delete_thread and delete_task at affected_rows 1 and delete_problem_statement_by_pk
    returning a row
    — the value Deleting a problem statement, task or thread leaves the row behind and reports success #99 could never get. The successful delete is itself the proof the
    FKs are CASCADE
    , since under the old RESTRICT it raises a foreign-key violation; that is the
    migration verified from the app, not from confdeltype. Confirmed as anonymous afterwards,
    not just in the app — an orphan is invisible to its owner and visible to anonymous, which is how
    Deleting a problem statement, task or thread leaves the row behind and reports success #99 hid. Run at localhost:3000 against TACC's production Hasura, not at the host, and the
    literal test was not available:
    mint.tacc.utexas.edu still serves Lit, whose delete removes the
    CREATE provenance as root field 6 before the row delete in the same transaction, so it fails for
    a reason unrelated to the migration — a false negative. Origin is not part of this round trip (same
    endpoint, same database, same user role); what localhost cannot prove is the deployment, which is
    Flip mint.tacc.utexas.edu to ui-react #77's job, and a re-confirm-after-flip constraint is landed there. Two things outlive this
    ticket.
    The migration alone does not fix delete — the client fix fix(ui-react): delete a problem statement, task or thread bottom-up (#99) #115 is required, and Lit
    stays broken, so a rollback to Lit is a rollback to silent orphaning. And a stale dev server from
    another worktree held port 3000, so two runs exercised day-old code that looked right and produced
    a confident wrong diagnosis; the request body gave it away, not the UI. Start dev servers with
    --strictPort, and read the request body before believing a live verification.
    That accident was
    a clean negative control — the old client against the migrated database reproduced Deleting a problem statement, task or thread leaves the row behind and reports success #99 exactly,
    HTTP 200 with no errors key — and it left two orphan rows at TACC that no client can delete,
    now #120.

  • Models step Continue does nothing once a thread has execution data
    fixed and verified live; the step now saves by diff, and Lit's answer was the wrong one to copy.
    Both faults held as written: the mutation deleted every thread_model row for the thread against
    four ON DELETE RESTRICT children, and ModelsStep had no catch, so the refusal was silent.
    diffThreadModels keeps a still-selected row and its id, deletes only deselected rows with
    their four children, and inserts only what is new — so an unchanged Continue writes nothing at
    all
    . Keeping the id is the whole point: the children hang off thread_model.id, so no
    delete-and-reinsert can preserve them. Lit clears the wall by deleting every child row for the
    thread on every save, which destroys the Datasets and Parameters work each time — not copied.
    Verified on thread x4gbmq00msmbp9ib under a real Tapis token, reading request bodies: unchanged
    Continue sent zero GraphQL requests and advanced the step; adding a model sent removedIds: []
    with all five deletes at affected_rows 0; removing it sent models: [] and delete_thread_model
    at affected_rows 1; and the kept row's id, data binding and total_runs 2 were untouched. TACC is
    back to its exact prior state. Three things outlive it. insert_<table>(objects: []) is
    accepted under the user role — Thread wizard dead-ends at Parameters: threadExecutionData is never loaded #104 implied it, this measured it. 21 of TACC's 109
    thread_model rows carry no modelcatalog_configuration_id
    , and the step deliberately leaves
    them alone: deleting one would hit the same RESTRICT wall on exactly the legacy threads that predate
    ui-react. And removing a model still discards its runs without warning — Lit does the same for every
    model on every save, so it is not a regression; ticket it if TACC hits it. Code in
    PR #121, 921 tests green, the 4 new assertions
    checked to fail against the old code. Flip mint.tacc.utexas.edu to ui-react #77 is down to one blocker,
    #110.

Not yet specified

Out of scope

  • Porting the un-ported Lit screens — Analysis, Emulators, Messages,
    models-compare/models-calibrate/models-cromo, thread Visualize, and the 3,256-LOC
    model-view. (Thread Summary was on this list in error — it is ported.) Never in the SOW,
    whose scope is the config workflow and the flattened form.
    T1 confirmed SUBSIDE can live without
    them; it does not port them.
  • Retiring ui/ entirely. Downstream of the above, and a separate effort.
  • ISI's mint.isi.edu cutover. Different host, different stakeholders, not this contract.
  • ADR-0001 leftover debt — old model/model_io/model_parameter tables, Fuseki
    references in docker-compose and CI, absent row-level security on modelcatalog_*.
  • Issue Results post-processing: indicator comparison across run ensemble #35 (results post-processing, indicator comparison across a run ensemble) — a
    feature, not migration work.
  • A written UAT walkthrough. Considered and declined; TACC gets the URL.
  • The same model-type fault in model-catalog-api
    #102: /models returns 44 of TACC's 55
    software from a stale subtype allowlist, and four custom-handlers.ts handlers are cut harder
    still, to 27. Real and measured, surfaced while resolving
    #98. Out of scope because nothing on the path
    to sign-off touches it: ui-react talks to Hasura directly and never calls that API (Commit .env.example and local-setup notes so ui-react runs for someone who is not Max #72 deleted
    the dead MODEL_CATALOG_API key), and the only consumer is the Lit UI, which stays unrouted and
    is being retired. Filed so it is not lost.
  • A ZIP component cannot run on Tapis
    #108: the Ensemble Manager's Tapis path
    JSON-parses has_component_location, so a model whose component is a localex ZIP (HAND) fails
    with an opaque 500. Surfaced on the first attempt at
    #92 and proved by negative control — the
    same 500, byte for byte, with the input dataset swapped. Out of scope because it is
    pre-existing server behaviour, Lit hits it identically, and nothing on the path to sign-off
    needs a ZIP component: Submit one thread run end to end against TACC's Ensemble Manager #92 finished by choosing a Tapis-backed model instead. It does not
    block Flip mint.tacc.utexas.edu to ui-react #77.
  • Three smaller defects found while resolving Submit one thread run end to end against TACC's Ensemble Manager #92, all filed and none on the route to
    sign-off. #109 — every execution row is
    written execution_engine: localex, including Tapis runs; the server does not read the column,
    so nothing breaks today. #111 — the Runs
    step prints 2 runs (1 input resources × 1 parameters); the count is right and only the printed
    factor is wrong. #112 — every resource with
    an empty URL shares the id d41d8cd98f00b204e9800998ecf8427e, the MD5 of the empty string.
    Every resource with an empty URL shares one id: the MD5 of the empty string #112 is the one to revisit if result registration ever starts writing resource rows.

Metadata

Metadata

Assignees

No one assigned

    Labels

    wayfinder:mapWayfinder map: the shared route for an effort

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions