Skip to content

Release from dev: Annotate and Home tabs rebuilt on the hypeR-native client, staging tooling, React app removed - #163

Merged
Cvicnaire merged 73 commits into
masterfrom
dev
Oct 14, 2026
Merged

Cvicnaire merged 73 commits into
masterfrom
dev

Conversation

@Cvicnaire

@Cvicnaire Cvicnaire commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Release: dev -> master. Checklist copied from #154 (state as of opening; kept current there). Pairs with the montilab/SigRepo 1.1.0 release PR; both merge in the same window and production rebuilds once afterwards. #151 is left out of this release by decision.


Release checklist for the next dev -> master pull request. Paired with montilab/SigRepo release issue montilab/SigRepo#267 (the client one). The two release pull requests merge in the same window, and production rebuilds its image once, after both are in. See the client issue for why the coupling runs both ways; in one line: server dev calls seven client functions that only exist on client dev, and client dev's runHypeR() breaks the run_enrichment MCP tool that server master still ships.

Staging is montilab.bu.edu running montilab/sigrepo:dev; every box below is ticked there unless it says otherwise.

Depends on the client release

  • Rebuild the Shiny Annotate tab on SigRepo::runHypeR() and open it #148 Shiny Annotate tab rebuilt on SigRepo::runHypeR(). On staging, run LLFS_Aging_Gene_2023 with hypergeometric, kstest and fgsea against MSigDB Hallmarks: results table, dot plot, enrichment plot, Excel export all work; the generated R script shown in the tab reproduces the call. Run a mouse signature: species is picked from the signature, not a dropdown default. The MSigDB cache is present in the image (startup does not warn "MSigDB cache file was not found").
  • Restyle the Shiny Home tab to match the retired React dashboard #153 Shiny Home tab restyled to match the retired React dashboard. Counts on the Home tab equal SELECT COUNT(*) on the staging database for signatures, collections and organisms; links from the tiles land on the right tabs.
  • Withdraw the MCP run_enrichment tool until it is rebuilt on the new runHypeR() #139 MCP run_enrichment withdrawn. The MCP tool list has no run_enrichment; rummagene_enrich, search_signatures, compare_signatures still answer. This is the change that makes the client release safe for production.

Staging and build tooling (server-only)

Removals

Process and CI

Release steps

Out of this release

  • Nothing on dev is excluded. A failed box becomes a bug issue linked here; this issue carries blocked until it closes.

🤖 Generated with Claude Code

Cvicnaire and others added 30 commits September 15, 2026 18:13
…sets

Pure helpers for the rebuilt Annotate tab (#83): runHypeR() defaults held to
the function's formals, hypeR-native gene list parsing and per-test checks,
every background form including per-signature lists, and genesets from the
MSigDB cache (list or table form), live msigdbr via getHypeRGenesets(),
hyperdb rgsets and custom files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing R code

Builds runHypeR()/prepareHypeRSignatures() arguments per signature source and
test, runs them keeping warnings, reads hyp and multihyp results into tables,
provenance, summaries and per-geneset enrichment details, sizes plotHypeRDots()
output, and writes the R script that reproduces a run and its plots (#83).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tab now only turns inputs into runHypeR() arguments and reads the result
back with SigRepo's hypeR functions (#83):

- Signatures from repository picks, uploaded OmicSignature .rds, or
  hypeR-native gene lists, one source per run, up to ten.
- Genesets from the MSigDB cache (live msigdbr when allowed), hyperdb rgsets
  or custom GMT/CSV/text.
- Every hypergeometric, KS and fgsea argument with its default in the label,
  per-signature backgrounds, and a query preview through
  prepareHypeRSignatures().
- Results: plotHypeRDots(), plotHypeREnrichment() with ES/NES and leading
  edge, plotHypeRMap() enrichment and hierarchy maps, a full-precision results
  table, per-query provenance, hypeR's own rctbl, the reproducing R code, and
  .rds and hypeRToExcel() downloads.

Removes the module's own query builders, dot plot, GSEA curve, GEM modes and
report bundle, hypeR_module.R, and the tests of the removed builders. Symbol
lookup now happens in the client, so the app no longer sources
api/lib/symbols.R.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- The Enrichment tab rebuilds its controls from the Results-table pick, so a
  row picked before the tab has opened, or a new run whose query names match
  the last one, no longer leaves the geneset dropdown empty.
- req() inside a drawing step reaches Shiny instead of showing a blank
  'could not be drawn' message.
- Integer browser inputs compare equal to runHypeR()'s defaults, so the R code
  no longer writes fgsea_args = list(sampleSize = 101L, minSize = 1L).
- An empty dot plot says which cutoff to raise; cleared plot controls fall back
  to their defaults; wider query dropdowns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dropdowns

hypeR draws enrichment maps with igraph's layout_nicely, which lines up every
unlinked geneset along one edge (most of a Hallmark map). The Maps tab offers
force-directed, circle and grid layouts as well, applied with
visNetwork::visIgraphLayout(), and the R code writes the same call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…(feedback)

From Cameron's Notes/Feedback/shiny_annotate_feedbac:
- hypeR rgsets dropped (too heavy for the droplet); MSigDB and custom genesets stay.
- Hypergeometric keeps split and min query genes; KS and GSEA keep direction only.
  Ranked table, score column, power, absolute, seed, fgsea sizes and the whole
  significance block are gone and stay at runHypeR()'s defaults, named in the help
  text under Background.
- Background is a primary option rather than hidden in a details panel.
- Results drop the Maps and Provenance tabs, and their helpers go with them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
montilab.bu.edu runs dev and needs an image whose R packages match it. Until
now a dev change that added a package could not be tested on staging until it
was merged to master, which defeats the point of the staging gate.

The dev trigger is gated in a new `decide` job rather than by a second `push:`
key with its own paths filter. YAML keeps only the last duplicate key, so that
spelling would have silently deleted the master build with nothing to say so.

On dev: build only when Dockerfile, install_r_packages.R or DESCRIPTION change,
tag :dev, bake SIGREPO_BRANCH=dev, and build only the sigrepo-api entry, since
all three filtered paths belong to its context and neither sigrepo-mysql nor
sigrepo-web is under test on the staging line. On master and main: unchanged.

The extra-repo tag is computed from the same value, so :dev reaches
montilab/sigrepo, which is the name docker-compose-local.yml resolves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…atest

montilab.bu.edu is the staging instance and runs the dev image, while every
other stack runs latest. Hardcoding :dev here was not an option: the Mac
development stack uses this same file, so it would have changed every local
stack on every machine.

Declared once as an x- anchor, beside the volume anchors this file already
uses, so the explanation lives in one place rather than three.

sigrepo-mysql and sigrepo-web are deliberately left on :latest: neither is built
from the branch under test, so staging runs the same ones as everyone else.

Verified that `docker compose config` output is byte identical to before when
SIGREPO_IMAGE_TAG is unset, and that setting it moves exactly sigrepo-api,
sigrepo-shiny and sigrepo-mcp.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Checks the failures that are silent and act at a distance: gene sets no longer
resolving because the source bind mount masks the cache baked into the image,
and Compare breaking because the image predates ComplexHeatmap, circlize, cba
and fgsea. Neither shows as a failed container or a non-200 response.

Shell, curl and python3 only, because montilab has no browser installed and its
host R package set is not guaranteed. The Shiny sign-in check lives separately.

Two failure paths matter as much as the happy path and are covered by
tests/fixtures/fake_api.py, which serves a valid but completely empty
repository:

  - a dead API fails in seconds rather than hanging, because a stalled deploy
    looks exactly like a slow one
  - an empty database is a clean FAIL, not a traceback and not a pass; a stack
    whose database failed to attach still answers 200 on every route

Containment is opt-in rather than silently skipped: a check that quietly does
nothing is worse than one that is absent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
HTTP 200 from the Shiny port proves Shiny Server is running. It says nothing
about whether the app works, whether the database is reachable from it, or
whether anyone can get in.

Separate from scripts/smoke_test.sh because montilab has no browser installed,
so the API checks run on the staging host during a deploy while this runs from
a machine that has one, through the SSH tunnel.

Three workarounds are load bearing and each carries a comment saying why:

  - a zero-width viewport suspends Shiny outputs, so the window is sized first
  - #sign_in_btn carries an inline onclick Enter-key helper that throws on a
    plain click, so the inputs are set with Shiny.setInputValue first
  - the login elements stay in the DOM after signing in, merely hidden, so the
    assertion is on the signed-in state rather than their absence

That last one is not theoretical: the tab bar renders identically signed in and
signed out, so a check asserting on tabs alone would pass with a bad password.
Verified that this one fails with a wrong password.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
camv is deliberately not in the docker group on montilab: the host is shared and
BU-managed, and the lab keeps no standing docker access on it. So a staging
deploy is one sudo line, and everything else lives in this script and its log.

What it refuses to do is the point:

  - it checks BOTH checkouts can fast forward before moving EITHER, because
    updating the server and then failing on the client leaves staging running a
    mismatched pair, which is worse than not deploying
  - it refuses to run without SIGREPO_IMAGE_TAG in .env, which would otherwise
    silently deploy the production image and make every later result misleading
  - it refuses an image tag that was never published, turning a confusing
    "manifest unknown" halfway through into a clear message before anything moves
  - it refuses to merge over uncommitted tracked changes
  - it never names sigrepo-local-mysql, so the seeded replica is never at risk
  - it fails the containment check loudly if any port leaves 127.0.0.1

tests/test_deploy_montilab_guards.sh exercises every one of those refusals and
asserts that none of them reaches a mutating step.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Staging is writable on purpose: production forbids adding, deleting or updating
signatures, so addSignature, updateSignature and the permission paths can only
really be exercised on montilab against real data. The cost is that test data
accumulates, and until now the reset was a hand-run mysqldump that existed only
as a memory of what was typed.

The guard matters more than the feature. This drops a database, and production
MySQL runs as "sigrepo-mysql", one character away from the "sigrepo-local-mysql"
this is for. The container name is checked against an allow-list first, before
the file is read and before docker is touched, so it cannot be aimed at
production by a typo or a stray environment variable.

It also refuses a corrupt dump before dropping anything, keeps a pre-restore
backup and names it in every later failure message, and verifies row counts
afterwards instead of trusting an exit status.

That last point is from experience: reading $? after a `grep -v` that filters
out every line reports failure on a successful restore, which is exactly what
happened on 2026-09-24. This reads PIPESTATUS for mysql itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CONTRIBUTING.md told people to branch with --base master. dev is now the
integration branch and is what the montilab.bu.edu staging instance runs, so the
documented workflow and the real one disagreed, and the documented one is the
one people follow.

States the rule the arrangement depends on: master gains commits only through a
release pull request, and a hotfix straight onto master is merged back into dev
the same day. That is what stops the two branches drifting apart, which is how
this arrangement usually goes wrong.

Also documents how to reach staging, which is through your own SSH tunnel with
your own BU account. Every port on montilab is bound to 127.0.0.1 and stays
that way: it is a shared, BU-managed host, the lab has been warned once about an
exposed port already, and the staging database holds real signatures behind
accounts that can edit them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…backup

Two defects, both on paths that only appear when things go RIGHT, which is why
neither showed up in the refusal tests.

A trailing `|| true` is a separate command in the AND/OR list. When the restore
succeeds, mysql exits 0 printing only its password warning, grep filters every
line and exits 1, so `true` runs -- and PIPESTATUS collapses to true's own
single element. Under `set -u` the next line, status=${PIPESTATUS[1]}, is then
a fatal unbound variable. The script died on restores that had just worked,
never printing the row counts or where the backup went. Moving `|| true` inside
a brace group keeps it out of the AND/OR list and leaves PIPESTATUS intact.

The pre-restore backup was written and never checked. gzip produces a valid
archive when its input produced nothing, so a failed mysqldump leaves a
plausible-looking two-byte file, and the script would drop the database anyway
and then point every later failure message at it. It is the only copy of
whatever staging held.

tests/test_reseed_staging.sh covers both, and drives the actual pipeline shape
so the trap is reproduced rather than described.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The compare check tested the response body for truthiness. The API answers
failures with json_error(), which serialises to [{"MESSAGES": "..."}] -- a
non-empty JSON list, and therefore truthy. So an image predating ComplexHeatmap,
circlize, cba and fgsea produced HTTP 500 "there is no package called
'ComplexHeatmap'" and the script printed PASS and exited 0. That is the precise
failure this check was written for, reported green. Now it requires a 200 and a
payload that is not an error envelope.

The containment check looked for the literal string 0.0.0.0, and only among
sigrepo-local containers. A bind to a routable address (128.197.229.35:8020->)
or to the IPv6 wildcard ([::]:8020->) contains no 0.0.0.0 and passed, and a
vm-compose stack published on 0.0.0.0 beside a local one was filtered out
entirely. It now asserts that every published binding IS on 127.0.0.1, across
every sigrepo container. On a shared BU host that distinction is the point.

Timeouts: the default 120s applied to enrichment and compare, on a 2-CPU host
with a single R worker, and deploy_montilab.sh passes no --timeout at all. A
slow-but-healthy staging instance produced two red checks and a deploy reporting
"not healthy". Enrichment and compare now get their own budget, and every
request gets --connect-timeout so a filtered host still fails in seconds rather
than burning the full budget.

tests/test_smoke_test.sh covers all of it, including the binding formats that
are awkward to produce on demand, via an overridable DOCKER_PS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Staged changes passed the preflight. `git diff --quiet` compares the working
tree to the index, so `git add` with no commit sailed through it -- and then
`git merge --ff-only` fails. Because the merge loop does the server first and
the client second, that left staging running a NEW server against an OLD
client: precisely the half-deployed state the two-phase check exists to
prevent. Now checks tracked changes with --untracked-files=no.

Untracked files break --ff-only too, but only when origin/dev is about to add
that same path, so only those are refused. Refusing any stray file would have
blocked every deploy on montilab, which legitimately carries an untracked
run_host_shiny.sh, and a guard people have to route around is not a guard.

Nothing checked which branch the checkouts were on. Every comparison is against
origin/dev, but `git merge --ff-only origin/dev` fast forwards whatever HEAD
points at, so on a checkout still on master it would have moved master to dev's
tip. That is the direct-to-master change branch protection and the hotfix rule
exist to prevent, and it would make "what is staging running" unanswerable from
the branch name.

A mistyped flag ran a real deploy. `--dryrun`, `--dry_run` and `-n` all fell
through to fetch, fast forward, pull and recreate. Pasted as a sudo line on a
shared host, silently upgrading "show me what would happen" into "do it" is the
wrong way to be wrong.

Also appends to the log instead of truncating it: the log of the deploy that
just failed is the thing you want when the retry fails too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
workflow_dispatch set build=true in the decide job but changed neither the
Docker Hub login step nor the build step's push: input, both gated on
github.event_name == 'push'. A manual run therefore warmed the build cache and
published nothing, which is the opposite of the reason it was added.

That is not academic. Merging this pull request into dev touches only a workflow
file, which is not in the paths filter, so no :dev image appears -- and
deploy_montilab.sh refuses to run without one, telling the operator to use
workflow_dispatch. Without this fix that advice was a dead end and the only way
out was the empty commit workflow_dispatch exists to avoid.

Also: after a force push to dev, github.event.before is unreachable and git diff
fails. The old code swallowed that and fell through to build=false, so a
force-pushed Dockerfile change would publish nothing and the next deploy would
quietly run an image that did not match the branch. It now builds when it cannot
tell, matching how the all-zero "before" of a first push is already handled.

test.yml's comment said the image build fires on master only and nothing
consumes a dev image. Both halves are now false.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…arded

matrix.build_args is a block scalar that already ends in a newline, and
sigrepo-web's is empty. Written as a two-line block, the rendered input for that
entry begins with a blank line, and whether docker/build-push-action discards it
is a bet with no upside. Concatenating yields exactly the arguments for all
three entries with no blank lines in any of them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…when-dependencies-change-on-dev

Publish montilab/sigrepo:dev when dependencies change on dev
…g-from-sigrepo_image_tag-defaulting-to-latest

Select the sigrepo image tag from SIGREPO_IMAGE_TAG
…he-staging-instance

Add an API smoke test for the staging instance
…n-check-for-staging

Add a headless Shiny sign-in check for staging
…ript-for-the-montilab-staging-instance

Add a one-command deploy script for the montilab staging instance
…-staging-database

Add a reseed script for the staging database
…-release-pull-request-and-the-staging-tunnel

Document the dev branch, the release pull request and the staging tunnel
GitHub auto-closes an issue only when the pull request merges into the
repository's default branch. Pull requests now target dev, so Closes #NNN does
nothing at merge time and fires when the release pull request carries dev into
master.

Observed on 2026-09-24: nine pull requests merged into dev across both
repositories, every one carrying Closes #NNN, and all nine issues stayed open.

The behaviour is defensible -- an issue open until its change is live means the
board shows what is in production rather than what is merged -- but the file
promised something different, and a rule people follow that does not do what it
says is worse than one that is honest about its timing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The suite ran in montilab/sigrepo:latest on every branch, which defeated the
point of building a dev image at all. A dev branch adding an R package would
build :dev correctly and then fail its own tests inside :latest, which lacks
that package, and the only way to go green would be to merge to master first --
exactly the loop the staging gate exists to break.

Deliberately held back until now: flipping this before a :dev image existed
would have made every run on dev fail to pull its container. montilab/sigrepo:dev
was published on 2026-09-24, so the precondition is met.

master and main are unaffected and keep using :latest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ose-at-merge-under-the-dev-model-they-close-at-release

Say when Closes #NNN actually fires under the dev model
…dev-image-once-one-exists

Run dev's test suite in the dev image
The first real staging deploy failed containment on sigrepo-activate-user
(8080) and sigrepo-rstudio (7878). Every container of ours was correctly on
127.0.0.1. Those two are other lab services on the shared host that merely
share the name prefix; they have been internet-open since long before SigRepo
staging existed and are not ours to change.

So every montilab deploy ended "staging is running the new code but is not
healthy" and exited non-zero, for a condition nobody can fix. A check that cries
wolf on every run is one people learn to ignore, and this is the check guarding
the constraint the lab has already been warned about once.

Matching only sigrepo-local-* is also wrong: it would hide a vm-compose or
default-compose stack of ours published on 0.0.0.0 beside a local one, which is
the case the prefix match was introduced for. Match our container names exactly
instead. They are enumerable and all three compose files agree on them.

Also fixes the test harness, which fed synthetic listings through an inline
"printf %s\n$fake". That mangled the tabs and newlines and word-split on the
ports, so the containment cases were passing for reasons unrelated to what they
claimed to test. They now go through a real file. Verified by mutation: putting
the over-broad match back fails exactly the two new cases and nothing else.

The montilab listing in the tests is the real one from the 2026-09-24 deploy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cvicnaire and others added 25 commits September 28, 2026 20:35
…e-bu-network-so-lab-members-need-no-ssh-tunnel
…b() step leaves no package

The first uncached build after #141 published an image with no SigRepo in
it. SigRepo's DESCRIPTION imports hypeR (>= 2.0.0) with no Remotes: entry,
so with hypeR absent at install time remotes fetched hypeR 2.0.0 from the
CRAN snapshot, which no longer builds against the current msigdbr. hypeR
failed, SigRepo was removed as "dependency not available", and because
install.packages() only warns, R -e exited 0 and the build went green. The
GitHub hypeR (2.11.1) then installed cleanly one step later.

hypeR and hypeR.GEM now install from GitHub before SigRepo, so its
resolution finds them already present. Every install_github() step ends
with stopifnot(requireNamespace(...)), matching the ComplexHeatmap line,
so a missing package fails the build instead of the containers.

Closes #144.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nterface-to-the-bu-network-so-lab-members-need-no-ssh-tunnel

Publish the staging Shiny interface on SHINY_BIND so BU-network users need no tunnel
…o-before-hyper-so-sigrepo-pulls-cran-hyper-200-fails-and-the-image-ships-without-sigrepo

Install hypeR before SigRepo and fail the build when an install step leaves no package
The hypeR client changed after the tab was rebuilt on it. runHypeR() now
requires organism and test, gives neither a default, and no longer takes
direction or absolute: kstest and fgsea always test both ends of the
ranking, as two queries, up and down.

The tab follows. Organism moves out of the MSigDB picker to the top of
Step 2 and is sent with every run, since it is now the organism of the
signatures and not only the species the genesets are loaded for. The
Direction control and the absolute setting are removed. Changing the
organism drops loaded MSigDB genesets and keeps custom ones, which do
not depend on it.

A signature of another organism now blocks a run with a message that
names it, where before it was a note against the loaded genesets.
runHypeR() stops on that mismatch, so the tab says so first.

The tests skip unless the installed SigRepo has the final client, which
they recognise by runHypeR() taking organism and not taking direction.

This was written on 2026-09-16 and left uncommitted in the worktree. It
is committed as it was found, after running the tab's tests against the
SigRepo dev client: 265 expectations, none failing, none skipped.

Refs #83.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GitHub honours closing keywords only on merges into the default branch.
Pull requests target dev, so issues stayed open after their pull request
merged. The workflow closes them at merge and CONTRIBUTING says so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e-into-dev

Close linked issues when a pull request merges into dev
The branch was cut on 2026-09-14 and dev has moved 84 commits since: the
type and platform rename, the React app's removal, the repository
cleanup and the staging image fixes.

Two conflicts. bootstrap.R sources both utils/annotate_utils.R, added
here, and utils/signature_utils.R, added on dev. annotate_module.R keeps
this branch's rebuild; dev's two changes to the old module applied the
rename to code the rebuild replaces, and the rename is applied to the
rebuild in the next commit.

Refs #83.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The tab has shown an under-development page since #86 while its rebuild
on SigRepo::runHypeR() was tested. This puts the rebuilt module back in
the navigation and restores its server call.

The signature table and the picked-signature summary read `type`, which
is what signatures.direction_type became in #124; they were written
before the rename and would have shown a blank. An uploaded .rds saved
before the rename still carries direction_type, so the upload summary
reads either and leaves it to runHypeR() to say why that file cannot
run.

tests/testthat/Rplots.pdf was a plot device's output committed by
accident with the rebuild, and is removed.

Checked in montilab/sigrepo:dev, the image staging runs (hypeR 2.11.1,
fgsea 1.32.4, SigRepo from dev): the tab's tests pass with 265
expectations and none skipped, and the tab was driven in a browser
against a copy of the repository for human, mouse, categorical and
proteomics signatures under each of the three tests.

Refs #83.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
test.yml checked out the SigRepo client with no ref, so every run took
the client's master, and it chose the image from github.ref_name, which
on a pull request is the merge ref and never "dev". A pull request into
dev therefore ran in :latest against the client's master: a pairing
that is never deployed.

For the Annotate tab that meant no test ran. Its tests skip on a client
without the rebuilt runHypeR(), which is every client but dev's, so 19
of 40 blocks, including every module test, would have skipped and the
job would have passed.

Both now read the line from github.base_ref on a pull request and
github.ref_name on a push: dev gets the client's dev and :dev,
everything else the client's master and :latest.

Refs #140.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
C2, and the HPO and GO:BP subcollections of C5, are no longer offered in
the picker. GO:CC and GO:MF stay.

A run happens inside the one R process every visitor shares, and R does
not hand freed memory back to the system, so a large run costs the app
and not the session. It is the same cause as #88 for the Compare tab.
Measured in montilab/sigrepo:dev with one signature, hypergeometric,
each on a freshly started app at 1.4 GB:

  H          50 genesets     3 s   1.5 GB
  C5 GO:CC   1,080          21 s   2.3 GB
  C5 GO:MF   1,872          41 s   2.6 GB
  C2 CGP     3,555          75 s   3.8 GB
  C5 HPO     5,793          the process was killed for memory
  C5 GO:BP   7,538          the process was killed for memory

The container had about 4.5 GB to itself. After the C2 run the process
was still at 4.0 GB twenty seconds after the browser closed.

The rows stay in the table and are filtered by ANNOTATE_MSIGDB_WITHHELD,
which takes a collection or a collection/subcollection, so offering one
again is removing an entry.

Refs #83.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The picker listed every MSigDB collection it knew, 44 choices across the
two organisms, of which the cache scripts/build_msigdb_cache.R produces
held 6. The rest, every mouse-native collection and human C1, C3, C4,
C6, C7 and C8, ended in "not in this server's MSigDB cache". It now
offers what is cached for the organism chosen, and an organism with
nothing cached is told so and pointed at custom genesets.

C5 is withheld whole, joining C2. GO:CC and GO:MF ran, but took the
shared Shiny process from 1.4 GB to 2.3 GB and 2.6 GB, and it does not
give that back.

On a server with the standard cache this leaves Hallmark, for human and
mouse, and custom genesets.

What is asked of the loader is checked against what is offered. The
dropdown kept the last organism's collection after the organism
changed, so choosing an organism with nothing cached and pressing Load
asked for that organism's Hallmark file; found by driving the tab in a
browser, not by the tests, which set the inputs directly. The check
also means a withheld collection cannot be loaded by a stale or crafted
input.

A server that allows live MSigDB fetches no longer widens the picker.
annotate_load_msigdb() can still fetch, but nothing in the tab asks it
for a collection that is not cached.

Refs #83.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ab-on-runhyper

Rebuild the Shiny Annotate tab on SigRepo::runHypeR() and open it
The Home tab gets the React landing page's layout: a thin title bar with
one "Browse signatures" button, four stat cards with colour-coded icons,
and three chart cards (organism donut with legend, assay type, top five
contributors) on the same washed-blue background and card styling. The
Quick Actions card and the footer go, as they did in React.

The totals and chart frames come from pure helpers in home_utils.R so
they are tested without a server; the module is tested under
testServer() against a canned signature frame.

Closes #152

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…to-match-the-retired-react-dashboard

Restyle the Shiny Home tab to match the retired React dashboard
…uture workers

The HTML5 UP page template under legacy_app/www, its scripts and sass, a vendored Font Awesome 5 that duplicated the Font Awesome 6 Shiny serves through icon(), duplicate images and five unreferenced stylesheets were never loaded by the app: 62 of the 69 files under www, 3.3 MB of 3.4 MB. The test, resource and login modules, the view modal, upload_modal_server() and a commented-out delete modal had no caller, and app.js carried handlers for markup the app no longer renders.

app.R used to source bootstrap.R, which sourced every module into the global environment, and then source every module again into its own environment. bootstrap.R is now the one loader and sources into the environment app.R gives it. It no longer attaches future and promises or calls plan(multisession), which nothing in legacy_app used and which started an idle R worker per CPU core (twelve, about 90 MB each, in a container on a 12-core Docker Desktop VM), nor rmarkdown (the report template went with #83) nor a sibling hypeR-GEM checkout (the rebuilt Annotate tab has no GEM modes). The hypeR-GEM install stays in the Dockerfile because the SigRepo client imports it.

Verified with the ten Shiny test files (660 passes before and after) and a logged-in walk through every tab on a preview container: every asset returns 200, every icon renders from Font Awesome 6, and the container runs a single R process.

Closes #155

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… title bars instead of hero banners

The app carried four visual systems: the Home tab's React tokens in home_dashboard.css, five other tabs each with ~100 lines of inline tags$style under their own class prefix (a gradient hero banner, drop shadows, uppercase tracked labels, five different gradients), a plain Bootstrap 3 navbar in another navy, and an Arial sign-in card over a photo. Feedback was unstyled and nothing styled the DataTables chrome.

www/assets/css/sigrepo.css now holds the whole look, with the Home tokens promoted to the root: wash #eaf1fb, surface #ffffff, ink #101828, accent #2563eb, border #d7e3f2, the system font. Every tab is a .sr-page with a thin title bar (title, one sentence, the tab's primary action) above cards; the modules share class names (.card, .sr-step, .sr-toolbar, .sr-summary, .sr-empty and so on) and carry no CSS. The navbar is ink with an accent underline on the active tab, and the Longevity Consortium logo lives in the navbar's title slot, which also removes the startup warning about a bare image child (it hides below 1100px, where it would sit on the last tab). The sign-in card, the register and profile modals, Bootstrap's inputs and buttons, selectize and DataTables (header band, hairline rows, quiet export buttons, accent-tint selected rows) take the same tokens. Uppercase labels are sentence case; the step pills stay on Annotate and Compare only.

app_style.css, sign_in_style.css, home_dashboard.css and the sign-in photo are removed. tests/testthat/test-shiny-stylesheet.R checks that no module carries inline CSS, that every tab renders inside the shared skeleton, and that every class the app emits has a rule in sigrepo.css. Verified with the eleven Shiny test files (688 passes) and a signed-in walk through every tab on a preview container at 800, 1000 and 1440 wide.

Closes #157

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…column dropdown

Review feedback on #158: the column visibility dropdown was hard to read. Buttons 2.x marks a shown column with the dt-button-active class and a darker gradient, and the stylesheet's button rules outranked that cue, so every column in the list looked the same. The colvis button now opens Buttons' fixed, multi-column layout with a title, and the stylesheet draws a box beside each column that is filled with a tick when the column is shown.

The two tests that looked for the bare colvis string now use colvis_offered(), which accepts the configured button as well.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…Enter key, password hash in the URL, log timer

Collections: grouped_collections() grouped collection_db() by its id columns, but a search that finds nothing returns a data.frame() with no columns and group_by() errored, so the tab showed "Error: [object Object]". It now returns the empty grouped shape, which DatatableFX() renders as "No data available."

Uploads: both upload handlers called addSignature() / addCollection() without the dialog's Private/Public choice, so every upload took the client's default. They pass it now, through an injected add function (add_signature_fn, add_collection_fn) so tests can see what reaches it.

Sign-in: the inline handlers called login_keypress(e), and e is not defined inside an inline handler, so every keypress threw and Enter did nothing; the button's onclick threw the same on every click. The handlers pass event and the button has no onclick. login_keypress() also sends both field values before it clicks: Shiny's text bindings debounce keystrokes, and a click straight after the last character reached the server before the password did.

Sign-in wrote the user name and an unsalted MD5 of the password into the URL hash, where it landed in browser history and access logs; nothing read it back. Both hash writes are gone. The keepalive flush that stops an idle tab greying out behind a proxy fires every 20 seconds instead of every 2 and no longer prints a dot to the log each time.

Tests: test-shiny-collection-module.R (empty frame, grouping, upload visibility), test-shiny-signature-upload.R (upload visibility both ways), test-shiny-shell.R (the handlers and the button). 705 Shiny tests pass. Verified on a preview container: Enter signs in with no console error and no URL hash, the empty Collections tab shows the message, and the log has no dots.

Closes #159

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nto an auth module

app_server.R was 993 lines, about 870 of them these flows written longhand: the same isolate-and-trim block twelve times, the email regex three times, three modals built inline, none of it tested. modules/auth_module.R now holds the sign-in card, the three modal builders and auth_server(), a plain server function (its ids stay bare because app.js and scripts/smoke_test_ui.R address them) that takes every database, API and mail-service call as an argument and returns the per-user handler and user row. The checks live in utils/auth_utils.R; validateUser() moved in from its own file. app_server.R keeps the session plumbing, the two repository frames and the tab wiring, 103 lines.

Registration and password reset reuse the API's register_new_user() and request_password_reset() from api/lib/user.R, which bootstrap.R sources, so the form and the /users routes behave the same: a reset never reveals whether an account exists, and an account whose admin notification fails says so instead of implying nothing happened. user.R gets its own scalar_text() in place of common.R's json_scalar(), because common.R opens the API's database pool when sourced.

The sign-in handler no longer falls back to the literal droplet address 142.93.67.157:8020 when API_HOST is unset; bootstrap logs a notice instead, and the installer and compose files always set it. One field_value() in utils.R replaces the identical helper in the Signature, Collection and Browsing tabs.

Tests: test-shiny-auth-helpers.R and test-shiny-auth-module.R (sign-in refusals and success with the handler built from the environment, sign-out, register, reset by username and by email, email and password changes), written first. 797 Shiny tests pass. Verified on a preview container: sign-in by Enter, the register, reset and profile dialogs with their messages, sign-out, no console errors.

Closes #161

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…template-files-unused-modules-and-idle-future-workers

Remove the Shiny app's dead template files, unused modules and idle future workers
…y-app-the-home-tabs-tokens-on-every-tab-title-bars-instead-of-hero-banners

One stylesheet for the Shiny app: the Home tab's tokens on every tab, title bars instead of hero banners
…-empty-state-ignored-upload-visibility-sign-in-enter-key-password-hash-in-the-url-log-timer

Shiny bug fixes: Collections empty state, upload visibility, sign-in Enter key, password hash in the URL, log timer
…ster-forgot-password-and-profile-flows-into-an-auth-module-with-shared-validators

Move the Shiny sign-in, register, forgot-password and profile flows into an auth module
Cvicnaire and others added 2 commits October 14, 2026 10:12
… on purpose

montilab publishes the Shiny port on SHINY_BIND=0.0.0.0 (#142), so the
containment check failed on 0.0.0.0:8051 and every staging deploy ended
red for an intended condition. --public-ports lists host ports allowed off
127.0.0.1; only the port is matched, so any other publish, and any other
port, is still caught, and the pass line names what was allowed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…low-the-ports-deliberately-published-off-loopback

smoke_test.sh: --public-ports allows the ports published off loopback on purpose
@Cvicnaire
Cvicnaire merged commit f53606d into master Oct 14, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant