Spaces:
Running
Running
github-actions[bot] commited on
Commit ·
01b8103
1
Parent(s): 04cce69
Deploy d0d1a1b
Browse filesAlphaFold failures now name their cause; drop the superseded mockups
Source: https://github.com/WINTER4000/turingDNA/commit/d0d1a1bee4bd36d3a803c847ba9065349794b20e
- dee/static/app.js +41 -8
- dee/static/index.html +4 -4
- tests/test_fold_structure_tool.py +41 -0
dee/static/app.js
CHANGED
|
@@ -4010,13 +4010,33 @@ function _afResize(viewer) {
|
|
| 4010 |
// Retry" affordance (the model DOES exist, EBI just blipped); a genuine
|
| 4011 |
// absence says so plainly. Never again claim "model doesn't exist" when the
|
| 4012 |
// truth is "EBI was momentarily down."
|
| 4013 |
-
function _afShowError(host, transient, retry) {
|
| 4014 |
if (!host) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4015 |
const wrap = document.createElement('div');
|
| 4016 |
wrap.className = 'alphafold-loading';
|
| 4017 |
-
|
| 4018 |
-
|
|
|
|
| 4019 |
: 'No AlphaFold model is available for this protein entry.';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4020 |
if (transient && typeof retry === 'function') {
|
| 4021 |
const btn = document.createElement('button');
|
| 4022 |
btn.type = 'button';
|
|
@@ -4046,7 +4066,9 @@ async function mountAlphaFoldViewer(hit, host, extraOpts) {
|
|
| 4046 |
|
| 4047 |
const molstar = await _afWaitMolstar();
|
| 4048 |
if (!molstar) {
|
| 4049 |
-
|
|
|
|
|
|
|
| 4050 |
return;
|
| 4051 |
}
|
| 4052 |
try {
|
|
@@ -4063,14 +4085,21 @@ async function mountAlphaFoldViewer(hit, host, extraOpts) {
|
|
| 4063 |
if (!await _afTryLoad(viewer, candidates)) {
|
| 4064 |
// exists === false → genuinely no model; otherwise treat as a
|
| 4065 |
// transient EBI blip and offer a retry.
|
| 4066 |
-
_afShowError(host, exists !== false, () => mountAlphaFoldViewer(hit, host, extraOpts)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4067 |
return;
|
| 4068 |
}
|
| 4069 |
const overlay = host.querySelector('.alphafold-loading');
|
| 4070 |
if (overlay) overlay.remove();
|
| 4071 |
return viewer; // hand the instance back so the caller can spin/pin/resize it
|
| 4072 |
} catch (err) {
|
| 4073 |
-
|
|
|
|
|
|
|
|
|
|
| 4074 |
}
|
| 4075 |
}
|
| 4076 |
|
|
@@ -6583,13 +6612,17 @@ if (_quitBtn) {
|
|
| 6583 |
_afApplyBg(viewer);
|
| 6584 |
const { candidates, exists } = await _afResolve(pdbUrl);
|
| 6585 |
if (!await _afTryLoad(viewer, candidates)) {
|
| 6586 |
-
_afShowError(host, exists !== false, () => _mountCrisprStructure(host, pdbUrl, residue)
|
|
|
|
|
|
|
|
|
|
| 6587 |
return;
|
| 6588 |
}
|
| 6589 |
const ov = host.querySelector('.alphafold-loading'); if (ov) ov.remove();
|
| 6590 |
_highlightResidue(molstar, viewer, residue);
|
| 6591 |
} catch (err) {
|
| 6592 |
-
_afShowError(host, true, () => _mountCrisprStructure(host, pdbUrl, residue)
|
|
|
|
| 6593 |
}
|
| 6594 |
}
|
| 6595 |
|
|
|
|
| 4010 |
// Retry" affordance (the model DOES exist, EBI just blipped); a genuine
|
| 4011 |
// absence says so plainly. Never again claim "model doesn't exist" when the
|
| 4012 |
// truth is "EBI was momentarily down."
|
| 4013 |
+
function _afShowError(host, transient, retry, reason) {
|
| 4014 |
if (!host) return;
|
| 4015 |
+
// The headline used to be the WHOLE message, which meant every possible
|
| 4016 |
+
// cause — a CORS block, a 404 on a stale model version, an aborted
|
| 4017 |
+
// timeout, a Mol* parse failure — was reported to the user as "EBI is
|
| 4018 |
+
// down". An audit found AlphaFold-DB up and serving 200s while this text
|
| 4019 |
+
// was on screen, so the headline was simply wrong and there was no way to
|
| 4020 |
+
// tell from the UI.
|
| 4021 |
+
//
|
| 4022 |
+
// Keep the friendly headline, but always carry the actual reason: one
|
| 4023 |
+
// line under it for the user, the full error in the console for whoever
|
| 4024 |
+
// is debugging. A retry button on a cause that retrying cannot fix is a
|
| 4025 |
+
// loop, and the reason is what tells them which they are looking at.
|
| 4026 |
+
if (reason) console.warn('[alphafold] ' + (transient ? 'transient' : 'absent') + ':', reason);
|
| 4027 |
const wrap = document.createElement('div');
|
| 4028 |
wrap.className = 'alphafold-loading';
|
| 4029 |
+
const head = document.createElement('div');
|
| 4030 |
+
head.textContent = transient
|
| 4031 |
+
? 'AlphaFold-DB did not return the model — this usually clears in a moment.'
|
| 4032 |
: 'No AlphaFold model is available for this protein entry.';
|
| 4033 |
+
wrap.appendChild(head);
|
| 4034 |
+
if (reason) {
|
| 4035 |
+
const why = document.createElement('div');
|
| 4036 |
+
why.textContent = String(reason).slice(0, 140);
|
| 4037 |
+
why.style.cssText = 'margin-top:6px;font-size:12px;opacity:.7';
|
| 4038 |
+
wrap.appendChild(why);
|
| 4039 |
+
}
|
| 4040 |
if (transient && typeof retry === 'function') {
|
| 4041 |
const btn = document.createElement('button');
|
| 4042 |
btn.type = 'button';
|
|
|
|
| 4066 |
|
| 4067 |
const molstar = await _afWaitMolstar();
|
| 4068 |
if (!molstar) {
|
| 4069 |
+
// The Mol* bundle itself never arrived — nothing to do with EBI.
|
| 4070 |
+
_afShowError(host, true, () => mountAlphaFoldViewer(hit, host, extraOpts),
|
| 4071 |
+
'the 3-D viewer library did not load (CDN or network)');
|
| 4072 |
return;
|
| 4073 |
}
|
| 4074 |
try {
|
|
|
|
| 4085 |
if (!await _afTryLoad(viewer, candidates)) {
|
| 4086 |
// exists === false → genuinely no model; otherwise treat as a
|
| 4087 |
// transient EBI blip and offer a retry.
|
| 4088 |
+
_afShowError(host, exists !== false, () => mountAlphaFoldViewer(hit, host, extraOpts),
|
| 4089 |
+
exists === false
|
| 4090 |
+
? 'the AlphaFold API has no entry for this accession'
|
| 4091 |
+
: 'every candidate model URL failed to load — tried '
|
| 4092 |
+
+ (candidates || []).length + ' (newest version first)');
|
| 4093 |
return;
|
| 4094 |
}
|
| 4095 |
const overlay = host.querySelector('.alphafold-loading');
|
| 4096 |
if (overlay) overlay.remove();
|
| 4097 |
return viewer; // hand the instance back so the caller can spin/pin/resize it
|
| 4098 |
} catch (err) {
|
| 4099 |
+
// Usually a Mol* parse/render failure, not a network one. Saying "EBI
|
| 4100 |
+
// is unreachable" here sends people to check a service that is fine.
|
| 4101 |
+
_afShowError(host, true, () => mountAlphaFoldViewer(hit, host, extraOpts),
|
| 4102 |
+
(err && err.message) || String(err));
|
| 4103 |
}
|
| 4104 |
}
|
| 4105 |
|
|
|
|
| 6612 |
_afApplyBg(viewer);
|
| 6613 |
const { candidates, exists } = await _afResolve(pdbUrl);
|
| 6614 |
if (!await _afTryLoad(viewer, candidates)) {
|
| 6615 |
+
_afShowError(host, exists !== false, () => _mountCrisprStructure(host, pdbUrl, residue),
|
| 6616 |
+
exists === false
|
| 6617 |
+
? 'the AlphaFold API has no entry for this accession'
|
| 6618 |
+
: 'every candidate model URL failed to load');
|
| 6619 |
return;
|
| 6620 |
}
|
| 6621 |
const ov = host.querySelector('.alphafold-loading'); if (ov) ov.remove();
|
| 6622 |
_highlightResidue(molstar, viewer, residue);
|
| 6623 |
} catch (err) {
|
| 6624 |
+
_afShowError(host, true, () => _mountCrisprStructure(host, pdbUrl, residue),
|
| 6625 |
+
(err && err.message) || String(err));
|
| 6626 |
}
|
| 6627 |
}
|
| 6628 |
|
dee/static/index.html
CHANGED
|
@@ -112,7 +112,7 @@
|
|
| 112 |
<!-- ?v= query bumps invalidate browser + iframe asset caches when app.css /
|
| 113 |
app.js change. Bump these numbers whenever you ship a frontend update —
|
| 114 |
without them, users keep getting the stale file for up to a week. -->
|
| 115 |
-
<link rel="stylesheet" href="/static/app.css?v=20260819-
|
| 116 |
<!-- The work catalog + the draggable rail. Kept out of app.css so two new
|
| 117 |
self-contained surfaces stay reviewable; every colour is an app.css
|
| 118 |
token, so both themes work with nothing added. -->
|
|
@@ -2907,9 +2907,9 @@
|
|
| 2907 |
<!-- Cloning reference data must load before app.js so the Designer
|
| 2908 |
can read VECTORS / ENZYMES / CLONING_METHODS / TAGS / LINKERS. -->
|
| 2909 |
<script src="/static/cloning_db.js?v=20260530-ui-polish" defer></script>
|
| 2910 |
-
<script src="/static/context.js?v=20260819-
|
| 2911 |
-
<script src="/static/lineage.js?v=20260819-
|
| 2912 |
-
<script src="/static/app.js?v=20260819-
|
| 2913 |
<!-- The decision trace, BEFORE cockpit.js: applyEvent calls TDTrace.push
|
| 2914 |
on the very first event, and both are `defer`, so document order is
|
| 2915 |
load order. Loading it after would drop the opening events of a
|
|
|
|
| 112 |
<!-- ?v= query bumps invalidate browser + iframe asset caches when app.css /
|
| 113 |
app.js change. Bump these numbers whenever you ship a frontend update —
|
| 114 |
without them, users keep getting the stale file for up to a week. -->
|
| 115 |
+
<link rel="stylesheet" href="/static/app.css?v=20260819-lineage2" />
|
| 116 |
<!-- The work catalog + the draggable rail. Kept out of app.css so two new
|
| 117 |
self-contained surfaces stay reviewable; every colour is an app.css
|
| 118 |
token, so both themes work with nothing added. -->
|
|
|
|
| 2907 |
<!-- Cloning reference data must load before app.js so the Designer
|
| 2908 |
can read VECTORS / ENZYMES / CLONING_METHODS / TAGS / LINKERS. -->
|
| 2909 |
<script src="/static/cloning_db.js?v=20260530-ui-polish" defer></script>
|
| 2910 |
+
<script src="/static/context.js?v=20260819-lineage2" defer></script>
|
| 2911 |
+
<script src="/static/lineage.js?v=20260819-lineage2" defer></script>
|
| 2912 |
+
<script src="/static/app.js?v=20260819-lineage2" defer></script>
|
| 2913 |
<!-- The decision trace, BEFORE cockpit.js: applyEvent calls TDTrace.push
|
| 2914 |
on the very first event, and both are `defer`, so document order is
|
| 2915 |
load order. Loading it after would drop the opening events of a
|
tests/test_fold_structure_tool.py
CHANGED
|
@@ -79,3 +79,44 @@ def test_fold_structure_surfaces_lookup_failure_honestly(monkeypatch):
|
|
| 79 |
out = t._tool_fold_structure({"gene_symbol": "ZZZ", "organism": "human"})
|
| 80 |
assert out["ok"] is False
|
| 81 |
assert "ZZZ" in out["error"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
out = t._tool_fold_structure({"gene_symbol": "ZZZ", "organism": "human"})
|
| 80 |
assert out["ok"] is False
|
| 81 |
assert "ZZZ" in out["error"]
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# --------------------------------------------------------------------------- #
|
| 85 |
+
# The failure UI must name the actual cause
|
| 86 |
+
# --------------------------------------------------------------------------- #
|
| 87 |
+
# An audit found AlphaFold-DB up and serving 200s (v6, ~0.3s) while the viewer
|
| 88 |
+
# was telling a user "AlphaFold-DB is temporarily unreachable". The headline
|
| 89 |
+
# was the WHOLE message, so every possible cause — a Mol* bundle that never
|
| 90 |
+
# loaded, a stale model version 404, an aborted timeout, a parse failure —
|
| 91 |
+
# was reported as an EBI outage, and nothing on screen could distinguish them.
|
| 92 |
+
def _app_js():
|
| 93 |
+
with open("dee/static/app.js", encoding="utf-8") as fh:
|
| 94 |
+
return fh.read()
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def test_the_error_ui_accepts_and_shows_a_reason():
|
| 98 |
+
src = _app_js()
|
| 99 |
+
assert "function _afShowError(host, transient, retry, reason)" in src
|
| 100 |
+
# Shown to the user, and logged in full for whoever is debugging.
|
| 101 |
+
assert "console.warn('[alphafold] '" in src
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def test_the_headline_no_longer_asserts_that_ebi_is_down():
|
| 105 |
+
"""'did not return the model' describes what we observed. 'is temporarily
|
| 106 |
+
unreachable' is a claim about a third party's uptime that we were making
|
| 107 |
+
without evidence — and that the audit showed was often false."""
|
| 108 |
+
src = _app_js()
|
| 109 |
+
assert "AlphaFold-DB is temporarily unreachable" not in src
|
| 110 |
+
assert "AlphaFold-DB did not return the model" in src
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def test_every_failure_path_passes_a_reason():
|
| 114 |
+
"""A reason parameter nothing populates is decoration. Each call site must
|
| 115 |
+
say which failure it is reporting."""
|
| 116 |
+
src = _app_js()
|
| 117 |
+
for reason in (
|
| 118 |
+
"the 3-D viewer library did not load (CDN or network)",
|
| 119 |
+
"the AlphaFold API has no entry for this accession",
|
| 120 |
+
"every candidate model URL failed to load",
|
| 121 |
+
):
|
| 122 |
+
assert reason in src, f"missing reason: {reason}"
|