Add DeltaSignal pathway perturbation UI - #155
Conversation
Keep LNG UUIDs separate in an interactive top-response graph, leave the Reactome diagram accessible beside the controls, and document the complete demo workflow.
|
I added the follow-up visualization from our discussion. Results now have a raw LNG logic-nodes-and-edges view alongside the Reactome overlay. It keeps UUIDs separate, shows the 40 largest changes plus the perturbed inputs, retains edges only when both endpoints are visible, and exposes mapping and solver details when a node is selected. I also changed the panel to a side drawer so the pathway stays visible on desktop, with a full-screen layout on narrow displays. I documented the complete demo and local setup in Commit: |
…ranch) (#198) * update env variables. * For Literature display, if authorName, use this, otherwise use author. * wip * update * Updates to enviornement and manual editing of the publication component. * feat(render): produce GIF and PowerPoint from the site's own renderer The two formats the Java exporter still owned, and the two that most obviously looked like the old site. Both now come from the render page, so a downloaded figure is the diagram the site draws. GIF is the expression animation: one frame per sample, 1s each, looping, and a single frame when there is no analysis. Encoding happens inside the browser -- a frame is tens of megabytes of pixel data and there is one per sample, so shipping them out to be assembled costs more than the finished file. The palette is built from every frame rather than the first, because one frame's palette shifts colours on samples whose values land elsewhere on the scale; that is why frames are drawn twice and never accumulated. Verified per frame rather than by file size: PMAIP1 goes dark purple at 0.2 to bright green at 5.2 across the four samples of a posted dataset. Capped at 2000px on the longest side. A diagram's coordinate space is around 6000px wide and a GIF pays for that once per frame -- uncapped, four samples came to 3.1MB. Now 735KB. PPTX carries the SVG with a PNG fallback. PowerPoint draws the SVG and its Convert to Shape turns the diagram into editable shapes. The alternative is emitting DrawingML per glyph, as the Java exporter does via Aspose: editable on open, at the cost of a second renderer to keep in step with the first and a commercial licence. One click is worth that trade. Two bugs found in the illustration code this reuses: - EHLD raster download scaled twice, once on the context and again through scaled destination dimensions, so a downloaded PNG or JPEG showed the top-left ninth of the illustration blown up to fill the file. - showAnalysisInfo assumed the analysis-info group contains a text element. Every other lookup there is guarded; this one threw part-way through and left the region half-decorated. Rasterising an illustration now lives in EhldService, shared by the download and the render page, since both need the styles inlined first: an EHLD's styling comes from the page's stylesheets and does not travel with the markup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * update package.json with curator website script. * feat(download): take GIF and PowerPoint from the render service Clicking GIF or PPTX in the download panel now gets a figure drawn by the site's own renderer instead of by the Java exporter's reimplementation of it. That was the last place a curator could download something that looked like the old site. The app builds its URLs from RENDER_SERVICE, which is the current origin plus /RenderService, exactly as CONTENT_SERVICE works -- so it follows whatever host the bundle is served from. proxy.conf.js maps that path to the service on loopback, and serve-prod.js reads the same table, so beta and the dev server both reach it with no Apache change. The analysis token travels with the request, which is the point of a GIF: one frame per sample rather than a still. So does the sub-pathway preference, so the checkbox means the same thing for a server-rendered file as for one the browser produces. Encoding the token needed care. The analysis service returns it already percent-encoded, and setSearchParam encoded it again -- "...%253D%253D", a different token to everything downstream and a separate cache entry. It happened to still render, because the render page passed the same mangled value back to the same service. Illustrations keep going to the content service for GIF and PPTX: it serves the same illustration file either way, so there is nothing to gain. Clamped every number that arrives in a query string, now that the path is publicly reachable. scale=50 asked for a 320-megapixel canvas and got it, which is worse than an error: one query string for a gigabyte of someone else's memory. Scale is capped at the default, and nothing has needed more. deploy/render-service/ carries the systemd unit and says plainly what is still missing before this fronts reactome.org: rate limiting at Apache, Apache serving the cache directly on a hit, and a cache key that changes per release. Docs use --token "$ANALYSIS_TOKEN" rather than an angle-bracket placeholder. GitGuardian read the placeholder as a CLI-option secret and raised an incident; there was no credential in it, and a variable reference reads as one to a scanner as well as to a person. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(render): draw the GIF at a size its labels can be read at The animation was sharp and the words in it were gone. A pathway's coordinate space is around 6000px wide and its font sizes are chosen for 1:1, so fitting it into 2000px scaled 8pt type to under 3pt and softened every arrow. Zooming in then magnified a downscale, which is not the same as detail. It renders at the diagram's own size now, and that is affordable because frames are differenced. 255 palette colours carry the picture and one index is kept back to mean "unchanged": every pixel equal to the previous frame becomes that index, written with disposal 1 so the previous frame shows through. Between two samples of an expression analysis only the node fills differ -- compartments, edges and every label are identical -- and a long run of one repeated index is what LZW compresses best. R-HSA-109606 over four samples: 3.1 MB undifferenced at full size, 966 KB differenced, against 735 KB for the unreadable 2000px version. Three times the resolution for 30% more bytes, and each extra sample now costs what it changes rather than what it contains. --max-size and ?maxSize= still cap it for anyone who wants a smaller file. Deployment is a container rather than a systemd unit, so a wedged browser or an out-of-memory kill costs one request instead of the feature. node:22 plus Chromium rather than a Playwright image: those carry three browsers and land around 3 GB, this needs one, and node:22 is already here as the app image's base. tools/render/ has its own package.json for the same reason -- four packages instead of the site's whole tree -- and render-deps.spec.ts fails if its pins drift from the root's, which matters most for Playwright, whose browser download is version-locked to the library. The container publishes no port. It is reachable from the app container and nowhere else, which keeps the property that matters: a render can only be commissioned through whatever fronts the site. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(render): make a renderer change reach a browser that already has a figure The full-size GIF shipped and curators kept getting the 2000px one. Nothing was wrong with the renderer: the old file was in Cloudflare and in their browsers, and neither was ever going to ask again. Figures are served `public`, so Cloudflare stores them and keeps serving what it stored with the max-age it stored it under -- a day. Reloading the page does not touch it either, because a download link's URL is not a page subresource and is never revalidated. And Cloudflare's own Browser Cache TTL overrides what the service sends: 300s went out as 4h. So the address has to change, which is what RENDER_VERSION in the app does. The service ignores the parameter, so both versions of a figure share one entry in its disk cache, and everything downstream sees a new resource. Bump it with RENDER_CACHE_KEY whenever the renderer's output changes; the table in deploy/render-service/README.md says which invalidates what. Also an ETag, from the same key -- everything that determines the bytes is in it -- answered before any render happens, so a repeat download is a round trip rather than a browser holding a stale figure or the box drawing one twice. Verified by clicking the button rather than by reading the code: the download comes out 5775x2366, and cf-cache-status is a MISS on the versioned URL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ci): let knip see the render tooling, and make the export theme explicit CI has been failing since the GIF work landed, and every failure -- including the two dependabot pull requests -- was the same two items: gifenc and fflate looked like unused dependencies. They are imported from tools/render, which knip was not configured to look at. It scans tools/**/*.mjs now, with the CLI, the service and the SVG harness as entry points, so the count is back to the baseline of 147 with more of the repo covered than before rather than less. MAX_FRAMES and renderUrl stopped being exported; nothing outside their own modules used them, and exporting a constant only makes it look like API. Also the theme, which was implicit and machine-dependent. DarkService defaults from localStorage and then from the browser's prefers-color-scheme, and the render page inherited whatever that decided -- so the same request could produce a light figure here and a dark one elsewhere, with one cache entry for both. ?dark=true now asks for dark and nothing else gives it. The dark palette is plumbed through and cached separately, but the download panel does not offer it, deliberately: it is a screen theme. Standalone it reads as muddy -- pale mauve compartments, and sub-pathway labels whose dark halos exist to sit on a dark canvas. Frame capture takes the diagram's own background colour rather than a hardcoded white, which is what made the dark case wrong in a way that would have looked like a renderer bug: a dark palette on a white ground is neither theme. And a regression caught on the way: setting the GIF's MAX_SIZE to 0 for "the diagram's own size" left PowerPoint's fallback raster computing scale from MAX_SIZE / longest, so it asked for a PNG at scale 0. The zip would still have cleared the size floor, because the SVG in it is the real picture -- a blank fallback nobody would find until the one viewer that needs it opened the file. It has its own constant now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(render): read the container's dependencies from the root package.json A second manifest with its own pins was the obvious way to keep the image small and the wrong one. Dependabot only watches the root directory, so a bump there would have left the service's copy behind -- and the drift test I wrote to catch that would then have failed every dependabot pull request until someone edited a file dependabot cannot see. The express bump waiting in the queue would have hit it immediately. The Dockerfile now picks the four packages it needs out of the root manifest at build time, so there is one set of pins and nothing to keep in step. That matters most for Playwright, whose browser download is version-locked to the library: a mismatch there fails in a way that reads as a rendering bug. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(render): illustrations export standalone, and drop the URL version stamp Two illustration bugs, both hidden by the fact that a diagram worked: A .png of any illustrated pathway was a 500 -- "this view cannot export PNG yet" -- because the export only knew how to ask a cytoscape instance. Illustrations are the top-level pathways, so it was the ones a report is most likely to want. They go through the same rasteriser the animation frames use now. A .pdf of one was a blank 674 bytes, caught only by the size floor. An EHLD declares width and height of 100% with no viewBox, so outside the page it has no intrinsic size at all, and its styling comes from the page's stylesheets rather than the markup. EhldService.svgMarkup writes both down, and the SVG export uses it too -- that one was shipping unstyled. And the version stamp goes away. Figures are `private, no-cache` now, so nothing reuses one without asking; the ETag makes a repeat download a 304 in ~1ms. It was introduced to defeat Cloudflare storing a figure for a day and serving it with the max-age it was stored under, which is real -- but it meant two constants that had to be bumped in step, and I bumped one and not the other within the hour, which produces the worst case: a new address answered from the old cache. One knob now, RENDER_CACHE_KEY, and nothing new gets stored anywhere. Bounded the cache while here. A figure averages a couple of megabytes and there are thousands of diagrams times five formats, on a host that also runs Tomcat, Neo4j and the site's builds with 4 GB free. Filling that disk takes the site down, which is far worse than paying for a render again -- everything in the cache is derived data. 2 GB by default, least-recently-used evicted first, and evictions are logged: a cache that silently discards what it was asked to keep looks exactly like one that is working. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(export): make the animated SVG's timeline clickable (#141) The timeline could only be held still by keeping the pointer on the button, which is what the issue means by "instead of just over". All three controls it asks for now work, and the animation is CSS keyframes, so each needed a different answer: - Play/pause on click. The button carries both icons and a class on the root decides which shows, so it says what clicking will do rather than what the animation is doing. Pausing sets that class and also pauses each animation through the Web Animations API -- the class alone holds the picture still, so in a browser without the API play/pause still works and only seeking is lost. - Clicking a segment seeks the whole picture there, by setting currentTime on every animation at once. Verified against the timing rather than by eye: segments land on 1s, 3s, 5s, 7s for four samples at 2s each. - Hovering a segment names the sample, through a native <title> tooltip. That needs no script at all, which matters because a script in an SVG only runs when the file is opened as a document -- not inside an <img>. The hover-to-pause rule is kept for that case and scoped so the two never fight. Two things found on the way: A sample name containing "<" silently truncated its label. The markup is built as a string and parsed by innerHTML, which turns "<baseline>" into an element SVG does not know and therefore does not draw, taking the rest of the label with it. "&" was already being normalised by the serialiser, so only "<" bit. Names are escaped now, and a column called `Ctrl & <baseline>` round-trips exactly. And console.table of every frame's transition times, on every export. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(reacfoam): signify flagging with a border rather than a fill (#140) Flagging used to repaint the flagged groups in the flag colour and, with no analysis running, wash every other group out to the surface colour. So a curator could see where a gene appears, or see their analysis result, but not both -- and the pathways where they most want the result are precisely the flagged ones. The flag is an outline now, drawn in groupContentDecorator by replaying polygonContext: that buffer holds the commands FoamTree used to trace the group's own polygon, so the stroke follows the real Voronoi shape rather than an approximating rectangle. The fill is left alone, so family colours and the analysis overlay both survive. Two strokes, a dark one under the flag colour, so it reads on a pale fill as well as a saturated one, and thinner at depth so a flagged child inside a flagged parent stays legible. The path is replayed once per stroke rather than stroked twice: on canvas either works, but the SVG export draws through svgcanvas, which records one path element per path and keeps only the last style set on it -- the halo was silently missing from every exported figure while looking right on screen. Checked in the exported SVG, not just on screen: both widths are there. Triggering moves to onSurfaceDirty, because flagging changes without the layout changing. The decorator returns immediately unless something is flagged, which is what keeps that affordable on a hierarchy this size. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor: make revealing the selected thing a directive (#137) The event hierarchy's auto-scroll was the good version of this behaviour, and it lived as a document.querySelector from the component after the tree finished building. Three other places had grown their own copy, each with slightly different behaviour and each reaching into a template it did not own by an id convention. RevealDirective inverts it: the element that knows it is selected reveals itself. Timing follows rendering rather than a guess about it, so a node that appears later -- as its branch expands, or when the tree is rebuilt for an analysis -- is revealed when it appears instead of racing whatever finished first. Applied to the hierarchy's nodes, the analysis result table's rows, and the pathway list under a selected search result. The hierarchy's fallback of scrolling to the pathway when the URL selects something that is not an event in the tree is kept, as `select() ?? pathwayId()`. `node.isSelected` was the obvious input and is the wrong one: it is also set on every ancestor of the selection, to draw the path. Two frames rather than one. One frame is enough for the element to exist and not enough for it to be where it ends up -- a table still expanding rows, or paging to a different page, moves after the first -- and revealing then left the row 28px past the edge of its container. Verified by measurement, not by eye: the hierarchy scrolls 469px to bring the selected event inside its container, the result row lands directly below the sticky header, and window.scrollY stays 0 in both, which is the page-drag that block:'nearest' exists to prevent. Also respects prefers-reduced-motion, which the copies did not. Left alone deliberately: found-table re-scrolls its parent row when its own content finishes loading, which is growth rather than selection and does not fit the directive; the object tree the issue mentions has no selection to bind to yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(curators): record what to check from this round Six new things for them to look at, one of which we cannot check ourselves: the PowerPoint file is structurally valid, but "opens in PowerPoint and converts to editable shapes" needs PowerPoint, and there is none on the build machine. That one is also a decision they may want to push back on, so it says what we traded and why rather than only asking them to click. Also records what is deliberately not offered -- dark figures -- and what fails together: GIF and PPTX come from a service running alongside the site, so if those two buttons break it is almost certainly the service rather than the diagram, and saying so saves a round trip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(idg): port the IDG protein-to-pathways search (/idg) "What does this protein have to do with Reactome?", ported from the Vue app on idg.reactome.org, reachable from a tile on the homepage next to ReactomeFIViz. The data did not have to move first, which is the reason this is a front-end change and nothing else. idg.reactome.org/idgpairwise is public, answers with Access-Control-Allow-Origin, and returns TANC1's 482 enriched pathways in 70ms. IDG_SERVICE is the one line that changes when the data moves here. The pathways it finds are ours, so the table links into our own Pathway Browser rather than carrying the IDG portal's GWT diagram widgets across -- those widgets are the thing this year's work replaced. Two endpoint names are misspelled in the service ("realtionships", "Pathays"). They are spelled that way here too, because the service is what has to answer. An empty result now says which kind of empty it is. checkTerm tells "we have never heard of this symbol" apart from "this protein has no enriched pathway in the datasets you picked", and those call for different next steps -- check the spelling, or add datasets. Handling the IDG server being down took two goes. Reading value() on a resource that failed throws, and a throw inside a computed the template depends on takes the render with it, so a 503 from IDG produced "nothing found for TANC1" -- the page blaming the gene for the server being unreachable. Every resource read is guarded by hasValue() now and failure is judged by status(), checked before the loading and empty-term branches because it can happen before anyone searches. Verified against a 503 and a refused connection, and that the healthy path still returns 482 rows with no page errors. Breadcrumbs title-cased the segment, so /idg read as "Idg". Acronym segments are upper-cased now -- idg, api, doi, faq, orcid, toc -- which is right for all of them and wrong for none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(serve): say "rebuilding" instead of throwing ENOENT mid-build `ng build --watch` empties and rewrites the output directory, so for the ten to twenty seconds a build takes there is no index.html to send. Requests landing in that window surfaced Express's ENOENT stack trace, which reads like the site is broken rather than busy, and gives whoever is looking at it no way to tell those apart. Adam hit exactly that while I was rebuilding beta. 503 with Retry-After is the honest answer -- the server is fine, the build is not there yet -- and the page refreshes itself so nobody sits reloading. Anything else that cannot be read is still a 500, because that is a real fault. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(idg): filter by functional interaction score, and show the distribution The table had 482 rows for TANC1 because I passed a p-value where the service wanted a score. `prd` is the functional-interaction cutoff and defaults to 0.9; 0.01 meant no filtering at all. Two modes exist and the service picks by whether datasets are named, which is worth knowing: with datasets chosen `prd` is ignored entirely -- TANC1 returns the same 482 pathways at 0.01 and at 0.99 -- and only the no-datasets path actually filters. The threshold cannot have a fixed default. The service's own is 0.9, and TANC1's best predicted interactor scores 0.891, so 0.9 returns nothing whatsoever for it. It starts at the 90th percentile of the gene's own distribution instead, says how many interactors that keeps out of how many there are, and says what the best score is -- so a threshold that returns nothing is visibly the threshold's fault. Under it is the distribution itself, 28 buckets, filled where they are kept and faded where they are not. TANC1's scores sit between 0.24 and 0.89 with a median of 0.52, which no fixed number would have suited. The table defaults to FDR <= 0.05 and lowest-level only, both untickable. TANC1 goes from 482 rows to 71, and the top of it is now NMDA receptors and postsynaptic transmission, which is what TANC1 is for. And the overlay this was missing: the kept interactors can be run through Reactome's own analysis, which returns a token, which colours Reacfoam and every diagram through machinery that already exists. It is not IDG's enrichment recomputed -- it is Reactome's overrepresentation of the interactor list -- and the button says so rather than implying the numbers match the table. The token came back percent-encoded and the router encoded it again, which is the same double-encoding that bit the download links. Decoded at the service boundary this time, so every caller can encode once. Still missing IDG's own diagram: a pathway-similarity network, 412 nodes coloured by weighted Target Development Level and 22,509 edges. The edge count needs a filter of its own before it is worth drawing, so it is not in this pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(idg): show Target Development Level, and accept lower-case symbols The table had p-value and FDR, which any enrichment gives you. What makes this the *druggable* genome is Target Development Level, and the page was throwing it away. It comes from the network endpoint, which returns the same lowest-level pathways the table does -- 267 nodes against 267 lowest-level rows for TANC1 -- with a weighted mean TDL per pathway. The scale runs tDark to tClin and low is dark: TANC1's enriched pathways run 1.69 to 3.50, with FCGR3A-mediated phagocytosis at the dark end and acetylcholine binding at the drugged end. As a column, and as a plot of significance against how well studied, where the interesting corner is high and to the left: enriched for this protein's interactors, and full of proteins nobody has drugged. Lower-case symbols found nothing. The service's index holds upper-case human symbols only -- checkTerm says TANC1 exists and tanc1, Tanc1 and Trp53 do not -- and the enrichment endpoint answers a lower-case term with zero pathways rather than an error, so "tanc1" looked exactly like a gene with no enriched pathways. Terms are upper-cased on the way in, which cannot collide with a mixed-case symbol from another species because none is in there, and UniProt accessions are upper-case already. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(idg): add the portal's three plots, and match its defaults (#idg) I had built one plot of my own choosing and left out all three of IDG's, which was the wrong way round. So I drove the real portal and recorded what its results page asks for, rather than guessing again: - **Interacting Pathway Plot** -- significance per pathway, coloured by top-level pathway. That is the "type of pathway" that was missing, and it is the thing a table of p-values cannot show: whether a gene's hits cluster in one part of biology or scatter across it. TANC1's cluster in Signal Transduction (18), Immune System (7) and Neuronal System (7). The mapping comes from getHierarchicalOrderedPathways -- 2,730 stId-to-top-level pairs, fetched once and shared. - **Genes vs Functional Interaction Score** -- how many predicted interactors survive each threshold, as a curve. More useful than my histogram for choosing a threshold, because it says what a move costs. - **Feature Summary** -- interactions per data source, coloured by kind of evidence. Its request wants gene *names* and dataset *id strings*, not the digital keys everything else takes; sending keys returns an empty list rather than an error, which is how I first concluded the endpoint was not the right one. Three defaults now match the portal, and the numbers agree exactly: threshold 0.8 rather than a percentile, so TANC1 has 11 interacting genes and 54 pathways where the portal shows 11 and 54; a Genes column, which the payload had as numGenes all along; and ten rows a page with a working pager rather than a hundred-row wall. The TDL plot I added stays. It is not in the portal's results page -- TDL appears there only inside the pathway overview -- but low TDL is what "druggable" means, and having it next to significance is the reason to look. Plots are positioned elements and one polyline rather than a charting library: every point is a link with a tooltip, the shapes are scatter and a line, and the repo has no charting dependency to justify adding one for that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(idg): drop an unused type import Left over from wiring the feature summary. My removal patch silently missed because prettier had already collapsed the import to one line, and I committed over the top of a lint gate that was one warning above the baseline -- exactly the failure I asked the asserts to catch, on the one edit where I did not check the gate afterwards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(idg): teach the e2e suite about the new shortcut and page CI failed and it was right to: the homepage test asserts the exact set of shortcut cards, and I added a sixth without telling it. That test exists because these cards have been silently mis-wired before, so the answer is to declare the card, not to loosen the count. Also a smoke test for /idg, deliberately stopping short of the results table. That data comes from idg.reactome.org, a different server from everything else this suite touches, and a test that needs it to answer would fail here whenever it is down while saying nothing about our build. What is guarded is the part we own: the route renders, and searching a lower-case symbol puts the upper-case one in the URL -- the bug Adam found by typing "tanc1". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(idg): expandable rows with the pathway's description and hierarchy The last thing the portal's results page has that ours did not. Clicking a row opens the pathway's description and its place in the hierarchy, both from our own content service -- which is what the portal does too, so an expanded row costs nothing from a machine we do not control. One row open at a time, as it has. The ancestry comes back with the pathway first and the root last, which is the opposite of how a breadcrumb reads, so it is reversed: Neuronal System → Transmission across Chemical Synapses → Neurotransmitter receptors and postsynaptic signal transmission → Activation of NMDA receptors, which is what the portal shows for the same row. Summations carry markup -- `<br>` between paragraphs, the odd `<i>` -- and my first version rendered "...Traynelis 2018).<br>NMDA tetramers assemble..." with the tag visible. Converted to text with real line breaks rather than passed through innerHTML: nothing in a description needs to be clickable, and text cannot inject anything. Also settles where the network view belongs. IDG's "Open Pathway Overview" leaves the results page entirely -- it opens their GWT browser at #/FLG=TANC1&DSKEYS=0&SIGCUTOFF=0.8 -- so the pathway-similarity network is a feature of that browser, not of this page. Our equivalent is the genome-wide overlay button already here. Nothing on the results page is missing now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(idg): list the interacting genes, with a CSV, and link out to Pharos The threshold note said "11 of 2541 predicted interactors score 0.80 or better" without ever saying which eleven. They are behind a collapsed summary now -- supporting evidence for the table rather than the point of the page -- strongest first, with their scores. For TANC1 they are SPTAN1, five tubulins, HOMER1, KRAS, SHANK1, DLG4 and CAMK2A: postsynaptic scaffolds, which is what TANC1 is. The CSV is built from what is already loaded rather than through the service's own download endpoint. The numbers are on the page to draw the list, so asking a second machine to derive the same file is a round trip and a failure mode for nothing. And a Pharos link per gene, which is where the Target Development Level in the table comes from -- so it is the right place to send someone who wants the evidence behind that number. Deliberately not included: the annotated-pathways tree. It is the one thing on the portal's results page we still do not have, and the one I would add next, but Adam's call was these three. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(idg): download the pathway list as a CSV The file carries every row the filters kept, not the ten on screen -- a file of ten because that happens to be the page size would be a surprise. 54 rows for TANC1 at the default threshold, which is what the heading claims. It also carries the weighted TDL column, which the portal's own download does not, because building the file from what is already loaded means it can include anything the page knows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(render): run the container's browser from a path its user can read The container built, started, reported healthy, and failed every render: "Executable doesn't exist at /home/node/.cache/ms-playwright/...". `playwright install` had run as root and put Chromium in root's cache, while the service runs as node and looks in its own. PLAYWRIGHT_BROWSERS_PATH puts it somewhere neither owns, chowned to node. The headless shell is named explicitly too, since that is the binary chromium.launch() actually wants. Worth remembering what the health check did not catch: it proves the HTTP server answers, not that a browser can start. Healthy and useless at the same time. Compose also had to match this box, where the site runs on the host rather than in compose: the container renders host.docker.internal:4200 and publishes 4310 on loopback for serve-prod to proxy, and no longer depends_on app, which would have started a second app container to fight for :4200. A deployment where both are containers drops the published port and uses the service name. Verified through the container: R-HSA-109606 GIF at 5976x3350 in 22s, PPTX 1.1MB, an illustration GIF in 4.4s, and beta serving a cache-busted request end to end with cf-cache-status BYPASS. The cache is on a volume and a repeat is a 6ms hit. The restart policy is configured but not proven here, and the README says so: a crash is hard to simulate honestly. docker kill is a manual stop, which docker does not restart; kill -9 1 inside is ignored because the kernel shields a namespace's init from its own signals; and signalling from the host needs root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: start turning the release checklist into a suite Adam's aim is that a green suite stands in for days of curator testing. This is the first slice of that, built from Appendix R3 rather than from what I happened to touch. RELEASE-TESTING.md is the new-UI successor to that document. Same ground -- front page, navigation, Pathway Browser, in-diagram search, details panel, context menus, the four analysis tools, downloads, post-release -- but every row says whether a test proves it, a human must do it and why, or it is a gap and therefore work. 53 rows, and honestly a minority are automated today. It should get shorter every release. Three specs, chosen because each covers something that has actually broken: nav-links crawls all 73 links in the rendered navigation and fails naming any that 404 or render an empty shell. This is the longest paragraph in the old document, and the curators' QA pass found Analyse Gene Expression 404ing and Species Comparison on a blank panel. The list comes from the rendered nav rather than nav-options.json, because the dropdowns are CMS content -- so a page an editor adds is covered without anyone updating a test. downloads checks what is *inside* each file. Every download bug this month passed "did something download" and fails this: a .jpeg holding PNG bytes, a blank 674-byte illustration PDF, an illustration PNG showing its own top-left ninth. GIF and PPTX come from the render service and skip with a reason when it is not running, since its absence says nothing about the build. in-diagram-search covers the USP10 case the document asks for by name, and was one of the four blockers that stopped the curators' pass entirely. Two traps are written down because I hit both: export URLs 403 without a Referer (protect-exporters.sh sheds crawler load that way, and it looks exactly like a broken feature under curl), and download buttons must be matched by their label's own text, since the icon ligature makes the button read "imageSVG". And the limit worth saying out loud: the suite runs against localhost:4200, so Apache rules, Cloudflare and TLS are outside it. Green means the application is right, not that beta is serving it correctly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(testing): correct three rows the last commit claimed were covered The in-diagram search rows still said "gap" after I committed a message saying a spec covers them -- a patch that missed because prettier had reflowed the table, which is the third time that has caught me today. Matched line by line instead of on exact text. Also records the tally, since a release gate should say how much of itself is real: 23 rows automated, 7 that genuinely need a person, 23 still to write. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: cover every top-level pathway and the interactor overlay diagram-coverage walks all 29 top-level pathways from the content service and fails naming any that draw nothing. This is the broadest guarantee in the suite: a change that breaks one family of diagrams while leaving the single pathway other tests happen to open intact passes everything else and fails here. It takes 1.1 minutes, against 29 clicks a person otherwise makes every release. The list is fetched rather than written down, so a pathway promoted to top level is covered the release it appears. interactors covers the overlay by looking at the diagram before and after, not at the button: a resource turning blue proves only that a button turned blue. Two rows of the release document turn out to describe features this UI does not have, rather than tests I had not written: there is no interactor confidence threshold and no interactor download, and no threshold concept anywhere in the interactor services or the URL state. The old browser has both. They are marked **missing** in RELEASE-TESTING.md -- a distinct status from **gap**, because one is work and the other is a decision -- and written into the curator report, since curators will look for them. 25 automated, 7 needing a person, 19 to write, 2 not built. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(diagram): let a reaction's input edges reach the reaction (R-HSA-6805479) On a reaction page the lines coming into the reaction stopped short of it, while the outgoing ones met it properly. The asymmetry is the clue. A connector's own segments end short of the reaction by design -- 21 to 29 units in a pathway diagram -- and the reaction's own backbone segments bridge the rest. addEdgeInfo walks those segments, forward along extraLine (from -> to) and backward along reverseExtraLine (to -> from). A pathway diagram records the input side pointing *towards* the reaction, so the forward walk follows it. The reaction exporter's layouts record it pointing *away*: for R-HSA-6805479 the backbone reads 267 -> 247 where a pathway diagram would read 2277 -> 2298. The forward walk finds nothing, and the edge ends 26 units out. Outputs were fine only because their orientation happens to suit the backward walk. So one step along a segment recorded in the opposite orientation is now allowed, guarded: the step is taken only if it lands strictly closer to the point the walk is heading for. That guard is what makes this safe where the cross-map fallback removed earlier was not -- a wrong-direction segment leads away from the stop and is refused, so a U-shape cannot be pulled out of shape. Verified against that exact risk rather than asserted. Rendering R-HSA-112307, transmission across electrical synapses -- the U-shape diagram the removed fallback used to corrupt, and whose K+ connectors have no segments of their own so this routine builds their whole path -- gives a byte-identical PNG before and after. R-HSA-109606 differs by 4 bytes of PNG compression and zero pixels. The only thing that changes is the reaction page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(analysis): make tissue enrichment reachable by the analysis service Every tissue analysis failed. The form posts a URL to the analysis service, which then downloads the sample itself, and the URL was hardcoded as https://127.0.0.1/ExperimentDigester/... -- an address the analysis service cannot fetch. It answered 422 for all of them. The URL is for the backend, not the browser, which is the whole subtlety: it has to resolve from inside the service doing the fetching. Nothing terminates TLS for 127.0.0.1 there, so plain HTTP to the Tomcat both services already share is what works -- verified directly against the service, which returns a token for the localhost:8080 form of the same URL and 422 for the current one. It is a constant alongside the other service URLs now, named DIGESTER_FOR_BACKEND so the next person can see it is not addressed from the page. Sending it out through the public hostname instead would also work, and would put an analysis-sized request through the hairpin that took Apache down with 522s once already. Verified end to end in a browser: choosing Adrenal Gland and pressing Next now produces a token in the URL, an overlay on the diagram, and no failed requests. Not verified: species comparison and the quantitative tools. My attempt to drive species comparison closed the form instead of running it, so I am not claiming either way. They do not share this bug -- there is no other hardcoded backend URL in the app, and those tools pass identifiers rather than a URL for the service to fetch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(detail): a reaction's downloads live with its figure, drawn by our renderer The reaction page listed SBML and BioPAX in a bar above the summation, nowhere near the diagram, and offered no figure formats at all -- the bar gated those on isPathway, so a reaction had no SVG, PNG, PPTX or SBGN. The current site lists all of them inside the panel that holds the figure: data exports at the left, figure exports at the right. So the bar becomes a two-group toolbar and the reaction diagram section takes a projection slot for it. Both copies are always projected, because projected content cannot sit inside an @if, and each renders only where it belongs -- one toolbar on the page either way. The figure formats now come from the render service rather than the old server-side exporters, asked for the reaction's own layout via a new ?view=reaction: the render page draws the same reaction-scoped diagram the page displays, so a downloaded picture is the picture on screen. `select` frames a curated diagram on one event, which is a different figure and still available. PNG keeps the three tiers as render scales. Found on the way: the service passed neither `select` nor `view` to the page, and the cache key was missing both, so every variant answered from one entry. 1098x463 reaction SVG, 549/1098/2196px PNG tiers, PPTX and PDF verified through the container; 16/16 downloads.spec.ts, 65 unit, lint 660, knip 147 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(detail): the data exports read as a document, not as stray braces data_object draws a pair of curly braces, which beside a list of format names reads as punctuation someone left behind rather than as "these are the data formats". A document sheet says the same thing without being a puzzle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(idg): a pathway link opens with the interactors overlaid Clicking a pathway in the IDG results opened the diagram with nothing highlighted, which loses the point of the click: the question being asked is where this protein's interactors turn up, and the answer is the overlay. Only the "overlay on the genome-wide view" button ever created an analysis, so every other link arrived with no token. The page now analyses the kept interactors once per gene list and every pathway link carries the token -- the plots, both table links, and the hierarchy inside an expanded row. The button reuses it rather than posting the same genes again for a second token. Also: the info tab's three headings were emoji, and the magnifier fell back to a missing-glyph box beside "Explore Reactome Pathways". They are material symbols now, like every other icon on the site. TANC1 -> Activation of NMDA receptors lands with 21/71 found and the results titled "Overrepresentation analysis results of TANC1 interactors"; e2e idg 3/3, 65 unit, lint 660, knip 147 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(download): the catalogue points at files that exist, in the release we serve Thirty-three of the 74 links on the download page were broken. The names were wrong, not the plumbing: the Guide to Pharmacology set is published as GtoP* and we asked for IUPHAR*; every PE mapping is .txt and we asked for .tsv; the interaction files live under interactors/; the figure archives are diagrams.svg.tgz rather than diagrams/homo_sapiens.svg.tgz; five other mappings had invented names (models2pathways.tsv, Reactome2OMIM.txt, ProteinRoleReaction.txt, disease_variant_ewas_mapping.tsv, humanPathwaysWithDiagrams.txt). The functional-interaction networks and the curator tool were built as <bucket>/current/<file>, a prefix the bucket has never had -- both are tools published under reactome.org/download/tools, and the curator tool ships as two installers rather than a jar. Complex_2_Pathway_human and Ewas2Pathway_human are published and were simply missing. Every name checked against release 97 rather than guessed. The release itself was also wrong. It was read once in ngOnInit, before the content service had answered, so the page fell through to a build-time constant and offered the previous release. It follows the database now -- and there is no constant to fall back to: a number baked into the bundle is stale from the first release after a deploy and looks exactly as authoritative as the real answer. So the homepage header and the download links say nothing about a release until the database has said, and the links stay inert until then rather than pointing at a URL nobody can resolve. 74/74 links return 200; download-files.spec.ts HEADs all of them and checks the badge against /data/database/version; 65 unit, lint 660, knip 147 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(icons): icons come from the release bucket, not from the dev backend Icons are released files, published at <version>/icons/svg. They were fetched from a hardcoded ICON_HOST of dev.reactome.org, because /icon/ is not proxied on beta or release -- so the site's own icons came cross-origin from a machine nobody would call part of the site, and beta depended on dev being up to draw them. One rule, exported, because two IconServices build icon URLs and they must not disagree: the bucket when the release is known, the old host until then and on curator, whose database is not versioned. The icon library, search results and icon detail page all go through it now. Non-EHLD figures stay where they are: they are not in the bucket under any path I could find, and they already serve from the site's own origin. 28/28 icon requests on /community/icon-lib now go to download.reactome.org and render; download-files.spec.ts fails if any icon comes from elsewhere; 3/3 e2e, 65 unit, lint 660, knip 147 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(analysis): what an analysis is for, not just that its wizard opens The existing spec drives each wizard to its last step and stops, so everything after "Analyse" was unverified: the counts beside the pathways, the table you filter, the two overlays that have no wizard of their own, and the files you take away. Four checklist rows that curators were clicking through by hand. Each test creates its own analysis. A token is server-side state, and a test that leans on another test's token fails alone and passes in a suite, which is the worst way round. Notes for the next person, from what cost time here: a material menu item concatenates its icon ligature into its text, so the Diseases filter reads "microbiology Diseases" and an exact match finds nothing; the Diseases toggle changes a total the page never shows and the species facet is disabled outright for a human gene list, so a filter test has to drive the FDR slider, which takes the table from 80 rows to 42. 5/5 analysis-results.spec.ts; RELEASE-TESTING now 34 auto / 7 human / 13 gaps 65 unit, lint 660, knip 147 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(diagram): a node with no graph entry no longer blanks the whole diagram Switching species to Mus musculus left an empty canvas with the rest of the page updated around it -- title, breadcrumb, hierarchy, legend -- which reads as "the species switch is broken". It was not the switch. The diagram builder logs "Missing graph data for node" and then dereferences that same value with a non-null assertion two lines later, so one unmatched node threw for the whole build instead of for itself. Every inferred species diagram has such nodes, so every non-human diagram drew nothing. The dereference is optional now: the node renders without its ChEBI structure and accession, which is what the log line was already telling us to expect. Found by writing the test for the species switch, which is the point of writing them. R-MMU-109606 and R-RNO-109606 both draw, and both are asserted so this cannot come back quietly; the sub-pathway hover row moves to human, because the tints are on a canvas and the sidebar's hover handler only scrolls long names. 4/4 diagram-behaviour.spec.ts; RELEASE-TESTING 36 auto / 8 human / 10 gaps 65 unit, lint 657 (new baseline), knip 147 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(detail): the contents of the panels, not just their presence The checklist rows for the molecules tab, protein structures and the reaction page were all "the panel exists" at best. These assert what is in them: the molecule groups with their counts and stoichiometry multipliers, the structure viewer naming its PDB entry, and a reaction's name, id, type, species, summation prose, every section heading, and a reference that resolves to PubMed. Two things worth writing down, both found while making these pass. Selecting an entity only shades the molecule list while that tab is already open -- arriving with ?select= set opens the entity's own details and the list is not offered at all, so the order of the two actions is the test. And the structure viewer's label and accession are separate elements, so its text reads "StructurePDB5JSN" and an assertion with whitespace in it never matches. 4/4 detail-contents.spec.ts; RELEASE-TESTING 39 auto / 8 human / 7 gaps 65 unit, lint 657, knip 147 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(content): a page that names a release gets the one we are serving The statistics page embedded release 95's figures while the site served 97, because the release number was typed into the markdown once and nothing brings it forward. Content asks for `{release}` now and the renderer substitutes what the database reports -- and a hardcoded number in a bucket path is rewritten too, since everything under the versioned bucket is republished per release, so the current one is always the right answer. Checked against production, which embeds the same two documents from the same place. Ours now says "Version 97, Panther, June 2026" like it does. Pages are only awaited on the release when their body mentions one, so the overwhelming majority still render without waiting on the content service. Also recorded, from writing these tests: the newest news announcement is V96 while the database serves 97, which is written content nobody can generate -- so that check is a marked expected failure, green in CI and loud the moment someone publishes the V97 item. And /tag/release 404s: it has not been ported. 5/5 content-currency.spec.ts; RELEASE-TESTING 43 auto / 8 human / 2 gaps 65 unit, lint 657, knip 147 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(news): import announcements from the current site, verbatim The V97 announcement was missing, so the site advertised V96 while serving 97. These are curated announcements -- release notes, contributor credits, statistics -- and the words matter exactly as written. Nothing here writes prose: the importer converts markup and then checks its own work, comparing the plain text of what it produced against the plain text of the source and refusing to write the file if a single character differs. That check earned its place four times over while importing V96 and V97: - a link target like `</images/x.png>` is indistinguishable from a closing tag, so the step that strips leftover markup ate every link on the page; - one announcement links "BlueSky@reactome.org " with the space inside the anchor, and trimming the label joined the link to the next word; - Joomla rebuilds hidden addresses from entity-encoded fragments, so reading the assignment up to the first semicolon returned one letter of the address; - Cloudflare hides others behind a link whose fragment is the address, wrapping the Joomla cloak on some pages. A dropped address cannot pass silently either: both sides of the comparison would lose it together, so the count of hidden addresses is checked against the count decoded. That is how V96's "Follow us on [email protected] get frequent updates" -- a sentence nobody wrote -- was found and fixed, along with the address at the end of the same file. V97 imported and V96 corrected, both text-verified; 6 unit tests cover the four traps; the news currency check is now an ordinary assertion 71 unit, lint 657, knip 147, e2e content-currency 5/5 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * build: stage content as part of the build, and check the PNG tiers differ Two loose ends. `npm run build` indexed the content but did not compile it, so a build picked up new announcements in the listing and served an empty page for them -- which is exactly what happened while importing V97, and the only reason it was noticed is that the page was checked afterwards. The deploy workflow and `npm start` both staged first; a bare build did not, and remembering that is not a plan. And the PNG menu's three tiers are now asserted to be three different pictures, by reading the width out of each file's header. Low, Medium and High returning the same bytes would be worse than offering a single PNG. RELEASE-TESTING: 45 automated, 7 human, 1 gap, 3 not built -- the tally line had drifted from the rows and is corrected here too 71 unit, lint 657, knip 147, downloads 17/17 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(figures): take figures from the release bucket, falling back to the origin Figures are the last asset the new site took from somewhere other than the release: they live in the legacy Joomla document root, which is why nothing under the versioned bucket ever resolved for them. They are requested from <release>/figures now, the same arrangement as icons and illustrations, with a fallback to our own origin so this is safe to ship before the release process publishes them -- verified on beta, which asks the bucket first, gets its 403 and falls back with nothing to redeploy once the files appear. Worth knowing while doing it: figures are broken on beta today. Our origin answers /figures/* with the application's index.html, so every one of the 883 figures the database names is a 200 that is not an image -- which also means a link check against our own origin proves nothing. Publishing them to the bucket fixes that; serving the legacy root through Apache would too, as a stopgap. Found a trap on the way: DetailFigureService declared `implements Partial<FigureService>`, so adding a method to the real service compiled cleanly and broke the detail page in the browser with "figure.src is not a function". A substitute for a token has to answer everything asked of the token, so it implements the contract now and the compiler will catch the next divergence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(figures): address figures outside the release, now that they are published Figures are not release artefacts. Icons and illustrations are regenerated each release, which is why the bucket versions those; a figure is a curated image the database references by a stable, version-free path, and the same file serves release 95 and 97. Keying the URL on the release would have copied 150MB into every release directory four times a year, and meant no figure could be addressed until /data/database/version had answered. So they live at the bucket root, and 2163 of them are now published there. 654 of the 665 the database references return a real image through the CDN; the remaining 11 exist on no host at all and are for curators to supply or to stop referencing -- five are named after papers, which is what a figure pulled for licensing looks like. This is the first time figures have worked on the new site. Our own origin answers /figures/* with the application's index.html, so every figure was a 200 that was not an image, and a link check against our origin would have called it healthy. The test asserts both halves: the host, so a fallback to the origin fails it, and naturalWidth, so HTML with a 200 on it fails it too. 654/665 through the CDN, verified drawn on beta; 71 unit, lint 657, knip 147 RELEASE-TESTING: 46 automated, 7 human, 1 gap, 3 not built Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: close the testing queue, and draw the pathway thumbnail with our renderer Loose ends, finished rather than listed. The pathway detail page's figure came from the content service's diagram exporter, which reimplements the drawing server side -- so the picture on the page was in the previous site's style while the download buttons beside it, which go through the render service, were in the new one. Two pictures of the same pathway on one page. It is the same renderer now, for cytoscape diagrams and for illustrations, with the old exporter kept as a fallback for a cold render or a service that is down. The last three items in the queue are closed too, two by testing and one by admitting it cannot be: - the participating-institute logos: each has an absolute destination that answers, and each logo really draws - the contents of the four pathway tabs that need no analysis (the other two are covered once an analysis exists) - the animated SVG timeline moves to human, like sub-pathway hover: it scrubs a canvas, and asserting pixels there fails on font rendering rather than on behaviour Also adds FOR-CURATORS.md, which is the outbound half of RELEASE-TESTING.md: what changed since the curators last looked and is worth re-testing, what is waiting on a decision only they can make, and the limits worth stating plainly. Both are updated in the same commit as the work they describe. RELEASE-TESTING: 49 automated, 8 human, none to write, 3 not built 71 unit, lint 657, knip 147, detail-contents 7/7 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(serve-prod): cover the server that actually serves beta It had no test. The e2e suite starts `ng serve`, so nothing exercised the express server real users hit -- and its behaviours are all edge cases, which is exactly why they need pinning: the catch-all that makes deep links survive a refresh, and the 503 that replaced an ENOENT stack trace during a rebuild. One of them is pinned as documentation rather than as a wish: a missing asset comes back as the application's own HTML with a 200 on it. That is unavoidable in a single-page app -- it cannot tell a bad asset path from a route -- but it is how every broken figure on the site passed for healthy, and anything checking assets has to read the content type rather than the status. DIST_DIR was added so the test can serve a directory it controls; unset, which is every real deployment, nothing changes. Written now because the express upgrade in #134 needs something to prove itself against. 6 tests, 395ms, hermetic (temp directory, free port) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * build(deps): express 4.18.2 -> 4.22.2 (dependabot #134) The only internet-facing package we own: serve-prod.js is what beta runs. It carried a high XSS advisory in response.redirect and a body-parser denial of service, both fixed here, and 4.22.2 is within the same major. Taken first of the eight open dependabot PRs because it is the one with real exposure -- the rest are build and authoring tooling. Proven by the serve-prod test written in the previous commit: same six behaviours, same result. express, body-parser, path-to-regexp, send, serve-static and cookie advisories all clear; 77 unit, lint 657, knip 147; beta answering in 70ms Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * build(deps-dev): playwright 1.58.2 -> 1.62.1, vitest 3.2.4 -> 3.2.7 (dependabot #129) Both patch-level, and both are what runs the tests -- so the proof is the suites themselves rather than anything written for the occasion. Clears the critical advisory on vitest's UI server. 77 unit; 50 e2e across the specs that cover today's work, the downloads, the catalogue, the homepage and IDG; lint 657, knip 147 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * build(deps): Tina 3.12 and its CLI, which is where the criticals actually were Dependabot offered tinacms 3.8.1 -> 3.11.0 (#133). Taking it would have changed nothing: both versions pin mermaid ^11, and the vulnerable mermaid@9.3.0 -> dompurify@2.4.1 came in under `@tinacms/cli@2.1.7`, which pins its own tinacms@3.5.1. The fix is the CLI: 2.6.0 depends on tinacms ^3.12, so the whole chain moves to mermaid@11.17.0 and dompurify@3.4.14. Also removes angular-cli-ghpages, which nothing used -- no script, no workflow, and the deploy is an aws s3 sync. It and its gh-pages dependency were the last two criticals. Its vestigial `PathwayBrowser:deploy` target goes with it rather than being left pointing at a builder that is no longer installed. 71 advisories -> 40, high 35 -> 16, critical 5 -> none 77 unit, 25 e2e across content pages and the editor, lint 657, knip 147 Covered first, in the previous commit: the editor loads for authors and is refused on a public origin -- an Apache rule nothing in this repository enforces, so an upgrade that quietly opened it would otherwise be invisible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(curators): where to look on beta, and what a correct result looks like A list of pages with the observation that matters on each, so a re-test pass is reading a table rather than reconstructing what changed. Includes the four things that look like bugs and are not, so nobody spends time on them: the figures that are missing everywhere, the deliberately refused editor, the page that was never ported, and the first render of a GIF being slow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(detail): the frame fits its column, the charts fit their box, BioPAX is one link Three things from a pass over beta. The frame around the reaction diagram had its right border cut off. Not the compartment and not the cytoscape fit, both of which were correct: the container is width:100% with a 1px border and no border-box, so the box came out 1070px inside a 1068px column and an ancestor with overflow-x:auto clipped the last two pixels. Two pixels reads as "the border is missing on one side" rather than as a layout bug, so it is pinned at two widths now. The statistics charts scrolled inside their own frames. The first is responsive -- 611px tall at 1070 wide, 457 at 800 -- and was in a 500px box; the second is a fixed 611 and was in 595. The first is sized by aspect ratio now, with enough slack for the ~18px of chrome that does not scale, and the second by the height it actually needs. Checked at four widths. BioPAX is a single link, Level 3. Level 2 has been superseded for years and the menu stood in front of the one people want. Also, from the same pass: the reaction diagram re-fits after fonts load and whenever its container changes width, which is correct regardless of the border bug; and the protein structure test accepts AlphaFold as well as PDB, because the viewer shows whichever resolved first. Whether an experimental structure should win when one exists is now a question in FOR-CURATORS rather than a guess here. 77 unit, 9 e2e on the detail pages, lint 657, knip 147 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(homepage): don't make the suite depend on four university websites The institute logos' destinations are checked locally but not in CI, where reaching them would tie the suite to their uptime and the runner's egress. The assertion that catches the failure we own -- an empty or mistyped nav entry -- still runs everywhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci: shard the e2e suite, which outgrew a single runner It passed the job's 30 minute limit today and was killed mid-run. A cancelled job reads like a hang rather than like "this suite got bigger", which is what actually happened: today's work took it from about 90 tests to 120. Four shards, split by playwright itself, so coverage is unchanged and the wall clock comes back to roughly what it was. fail-fast is off so one shard failing still tells us about the other three, and each uploads its report under its own name -- same-named artifacts collide and the upload fails exactly when the report is wanted. Co-Authored-By: Claude Opus 5 <…
…nt flag (#202) * feat(deltasignal): the pathway perturbation UI, behind a per-deployment flag Brings #155 onto main. The feature is Chryseisliu's; what is added here is the flag and the update to current main. Taken as content rather than as a merge. The branch was 139 commits behind, and #198 -- which merged main into it and added the flag -- was squashed into it, which flattened main out of its history. The content was then correct and the history was not, so a further merge conflicted in six files for no useful reason. These 23 files are the branch's own diff against main, applied to main. .github/workflows/tests.yml was deliberately left behind: the branch still carries actions/setup-node@v4 and upload-artifact@v4, and taking it would have reverted the two dependabot bumps that landed on main since (#125, #127). The flag: SiteProfile.deltaSignal -> SHOW_DELTASIGNAL -> viewport.showDeltaSignal -> @if around the Perturb button and the panel. Absent means off, so a deployment opts in rather than inheriting a research feature. Only `development` has it on, because /api is routed by proxy.conf.js -- the dev server's proxy, which does not exist in a built artifact and which nothing in deploy.yml or the Apache config provides. Still true, and still the thing between this and a deployed DeltaSignal: the service calls /api/pathways, /api/parse and /api/solve as bare relative paths with no environment indirection, where every other backend here goes through a constant. On a built site those three reach the origin and get index.html back. The flag makes that safe rather than fixed -- the feature is not offered where it cannot work. 210 unit tests, check:types, both ratchets unchanged, and e2e/deltasignal-toggle.spec.ts passes in both states. Co-authored-by: Chryseisliu <Chryseisliu@users.noreply.github.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(deltasignal): default the proxy at the port DeltaSignal is actually on DELTASIGNAL_BACKEND defaulted to http://localhost:8080. On the Reactome dev host 8080 is the Tomcat serving ContentService and AnalysisService, so /api/pathways, /api/parse and /api/solve went there and came back 404 -- a real server denying a real request, which reads as a broken feature rather than an unconfigured one. DeltaSignal's own compose also binds 8080, so the two cannot coexist on this box. It is published on 8090 here; the container still listens on 8080 inside. Verified end to end rather than by reading the config: the backend is up (GET /api/health returns deltasignal-api 0.1.0, Julia 1.10.10), the Angular dev server proxies /api/pathways to it with a 200, and on the development profile the Perturb button renders, the panel opens, and it fetches 200 /api/pathways. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Chryseisliu <Chryseisliu@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
What changed
/api/pathways,/api/parse, and/api/solveendpointsWhy
This is a bounded, demonstrable vertical slice for pathway perturbation prediction in the Reactome UI. It keeps the DeltaSignal engine in its own repository and uses the documented HTTP API as the integration boundary.
The first supported demo is Cell Cycle Checkpoints: selecting activated ATM (
R-HSA-5693527) maps to 29 logic-network nodes and can be run as a knockout to visualize the predicted downstream response.User impact
Users can go from a molecule selected in a Reactome pathway to a solver-backed predicted pathway response without leaving the Pathway Browser. Loading, unsupported-network, convergence, and API-error states are shown explicitly.
Validation
npm test: 69 tests passednpm run build: production build passedDeliberate follow-up scope
This draft does not yet include multi-pathway joining, ReacFoam-wide overlays, temporal ordering, backend deployment configuration, or the benchmark report. The largest local network currently has noticeable parse/solve request latency, so backend caching/deployment should be reviewed before production rollout.