From 50c4d83bd42348cdb4773ae6ba3cc5899ec686c8 Mon Sep 17 00:00:00 2001 From: Swaraj Patil Date: Tue, 2 Jun 2026 15:48:31 -0400 Subject: [PATCH 1/6] Protein-turnover template followups (3 UI subtasks) - Load page: add "Download MSstats-format CSV" button per converter, enabled after conversion, written via data.table::fwrite. - QC/help text: state both imputation preconditions across the QC description, MBi tooltip, and help page. - Statmodel: hide contrast-matrix header and workflow bullet for the turnover and chemoproteomics templates, gated by app_template(). --- DESCRIPTION | 1 + PREDESIGN.md | 123 +++++++++++++++++++++++++++++++++++ R/module-help-ui.R | 7 +- R/module-loadpage-server.R | 14 ++++ R/module-loadpage-ui.R | 11 +++- R/module-qc-ui.R | 4 +- R/module-statmodel-server.R | 9 +++ R/statmodel-ui-comparisons.R | 1 + R/statmodel-ui-headers.R | 7 +- man/MSstatsShiny.Rd | 1 - 10 files changed, 171 insertions(+), 7 deletions(-) create mode 100644 PREDESIGN.md diff --git a/DESCRIPTION b/DESCRIPTION index b1b8d96..3637c4d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -44,3 +44,4 @@ Encoding: UTF-8 Roxygen: list(markdown = TRUE) Config/testthat/edition: 3 Config/roxygen2/version: 8.0.0 +RoxygenNote: 7.3.3 diff --git a/PREDESIGN.md b/PREDESIGN.md new file mode 100644 index 0000000..5009574 --- /dev/null +++ b/PREDESIGN.md @@ -0,0 +1,123 @@ +# MSstatsShiny scaling: cost breakdown + architecture options + +## 1. Current AWS spend + +Cost Explorer, 3-month average (Feb-Apr 2026), `olga-vitek-lab` account, `us-east-1`: + +| Service | Monthly avg | Notes | +|---|---|---| +| EC2-Instances | $27 | One `t3a.medium` running 24/7 (`i-0908994d2d2186f7a`, "nu-ov-lab-us-MSstatsShiny") | +| EC2-Other | $22 | EBS volumes + Elastic IP | +| VPC | $10 | Public IPv4 charges and/or VPC endpoints | +| S3 | $8-15 | Lab buckets; April spiked to $14.87 | +| Route 53 | $0.50 | Existing hosted zone for `msstatsshiny.com` | +| All others | $0 | CloudWatch, KMS, SNS, SQS - unused | +| **Total** | **~$70/month** | | + +One thing from the audit worth flagging: the existing CloudWatch `StatusCheckFailed` alarm targets a stale instance ID (`i-068bfdd11cb2703f2`), not the live production instance. The lab has no working monitoring on the site today. The new design fixes this as a side effect. + +--- + +## 2. Projected cost for the new architecture + +The new setup adds an ALB, Fargate, WAF, CloudWatch logs, and ECR. Both architecture options share the same fixed costs. + +### 2.1 Shared fixed costs + +| Component | Monthly | Notes | +|---|---|---| +| Application Load Balancer | ~$22 | $16.43 hourly + ~5 LCU | +| Route 53 | $0.50 | Existing zone | +| ACM certificate | $0 | Free, auto-renewing | +| ECR storage | $0.50 | ~5 GB of image versions | +| CloudWatch Logs | ~$5 | ~5 GB/month ingested | +| S3 (ALB access logs) | ~$1 | | +| AWS WAF | ~$8 | ACL + 3 managed rules + ~1M requests | +| **Subtotal** | **~$37/month** | | + +### 2.2 Variable cost: Fargate compute + +Sizing assumptions: +- **Option A**: ShinyProxy dispatcher always-on at 0.5 vCPU / 1 GB; user tasks spawned on demand at Fargate's floor of 0.25 vCPU / 0.5 GB. +- **Option B**: 2 tasks always-on at 1 vCPU / 2 GB for HA; autoscales up to 8 tasks under load. + +Usage pattern: site mostly idle, with one ~5-day short course per month (~50 concurrent users for ~8 hours/day). + +| Scenario | Option A | Option B | +|---|---|---| +| Idle month | ~$55/mo | ~$109/mo | +| Light steady traffic (~5 users) | ~$60/mo | ~$109/mo | +| Course month (one 5-day course) | ~$80/mo | ~$115/mo | +| Course month at hard cap (75 concurrent) | ~$95/mo | ~$125/mo | + +Option A is $30-50/month cheaper because user containers only run when users are active; Option B carries a ~$72/mo always-on baseline for its 2 pre-warmed tasks. + +**Caveat**: these numbers assume the sizing above. CloudWatch shows the current EC2 at 0.3% baseline CPU - dramatically over-provisioned. Local profiling (in progress) will likely let us shrink task sizes and cut costs further. + +**On the $150-200 framing**: when I floated $150-200 as a budget envelope earlier, that was an engineering-side guess - not a target the architecture forces. Realistic operating range is $80-125/month, so you can set a tighter ceiling if that fits the lab's grants better. + +--- + +## 3. Cost vs concurrent users + +Marginal cost of adding concurrent users (sustained for an 8-hour course session), on top of the baseline: + +| Concurrent users | Option A | Option B | +|---|---|---| +| 1 | +$2 | $0 (within baseline) | +| 5 | +$5 | $0 (within baseline) | +| 10 | +$10 | +$1 (one extra task) | +| 25 | +$25 | +$3 (one to two extra tasks) | +| 50 | +$50 | +$6 (three extra tasks) | +| 75 (hard cap) | +$74 | +$12 (six extra tasks at cap) | + +Option A scales linearly (each user = one task). Option B scales in steps (one task serves ~10 users, TBD with profiling). At 50 users, Option B is much cheaper per user; below ~10 users, Option A wins because nothing runs when nobody's active. + +--- + +## 4. ShinyProxy on Fargate: Option A vs Option B + +The key choice: do we keep ShinyProxy as a per-user dispatcher (current model, ported to Fargate), or replace it with multiple parallel Shiny tasks behind ALB sticky sessions? + +### 4.1 Option A - ShinyProxy on Fargate (one container per user) + +ShinyProxy runs as a long-lived Fargate task. When a user connects, it spawns a new Fargate task running the Shiny container, routes the user to it, and tears the task down when they disconnect. + +**Pros** +1. **Per-user isolation.** Each user gets their own container - one user's heavy computation, crash, or memory spike doesn't affect others. Per-user resource caps are possible. +2. **Lower idle cost.** Only the lightweight dispatcher runs 24/7; user containers consume nothing when nobody's active. +3. **Matches the current operational model.** Minimal mental shift for the team; the `application.yml` mental model carries over. + +**Cons** +1. **Cold-start latency.** Fargate task spin-up is 30-60 seconds. Every new user waits on a loading screen on first hit - bad UX for a demo site where first-time visitors are the whole point. +2. **~3× the CDK complexity.** Needs a ShinyProxy task definition, IAM role for dynamic ECS task launches, networking for spawned tasks, plus ongoing `application.yml` management. +3. **Open-source dispatcher, no AWS support.** Integration issues between ShinyProxy and the Fargate ECS backend are on us to debug. +4. **JVM and ECR overhead.** ShinyProxy adds 500 MB - 1 GB memory baseline regardless of users; every spawned session pulls the image. + +### 4.2 Option B - Multi-task Shiny service with ALB sticky sessions + +Multiple Shiny Fargate tasks run in parallel, pre-warmed. The ALB uses cookie-based stickiness to bind each user to one task for their session. ECS autoscales task count on CPU and request volume. + +**Pros** +1. **No cold-start.** Tasks are pre-warmed; users get instant page loads. +2. **Simpler overall.** Standard ECS service pattern, ~1/3 the CDK code of Option A, standard CloudWatch metrics, no dispatcher single-point-of-failure. +3. **Native autoscaling.** ECS scales on CPU / request count; no custom dispatcher logic. + +**Cons** +1. **Shared R process per task.** Multiple users share one task's resources, with no per-user caps. A heavy computation slows others on the same task; a crash impacts all ~10 users on that task. +2. **Sticky session fragility.** Users who clear cookies or switch networks mid-session may be re-routed and lose state. +3. **Higher idle cost.** 2 tasks run 24/7 for HA even with zero users. +4. **Capacity planning needs profiling.** "Users per task" is determined empirically. + +--- + +## 5. Two decisions requested + +**Decision 1 - Architecture: Option A or Option B?** + +Both fit comfortably under recommended budget. The trade-off is cold-start UX (Option A's biggest con) vs per-user isolation and lower cost (Option A's pros). + +**Decision 2 - Cost ceiling.** Three reasonable framings, all with hard caps and AWS Budgets alerts to prevent runaway spend: +- (a) **~$125/month** - tight ceiling, optimizes for cost +- (b) **~$150/month** - comfortable for either option +- (c) **~$200/month** - generous headroom for future growth \ No newline at end of file diff --git a/R/module-help-ui.R b/R/module-help-ui.R index f3b697b..fea37d1 100644 --- a/R/module-help-ui.R +++ b/R/module-help-ui.R @@ -80,7 +80,12 @@ helpUI <- function(id) { experiments should likely use a subset of features due to the very \ large number of available features."), tags$li("Missing value imputation (can be MAR/MNAR depending on \ - spectral processing tool used.):"), + spectral processing tool used). Imputation runs only when \ + (a) the protein has at least one observed feature in the \ + current run, AND (b) the missing feature is observed in at \ + least one other run. Proteins entirely missing from a run \ + and features never observed in the dataset are not \ + imputed. Censoring assumptions:"), tags$ul( tags$li("Assume all NAs as censored (ie missing not at random)"), tags$li("Assume all intensities between 0 and 1 as censored and NAs as \ diff --git a/R/module-loadpage-server.R b/R/module-loadpage-server.R index 68aabc9..5f284b1 100644 --- a/R/module-loadpage-server.R +++ b/R/module-loadpage-server.R @@ -563,6 +563,20 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE, app_templa ) }) + observeEvent(get_data(), { + req(get_data()) + shinyjs::enable("download_msstats_format") + }) + + output$download_msstats_format = downloadHandler( + filename = function() { + paste0("MSstats_format-", Sys.Date(), ".csv") + }, + content = function(file) { + data.table::fwrite(get_data(), file) + } + ) + get_data_code = eventReactive(input$calculate, { getDataCode(input) diff --git a/R/module-loadpage-ui.R b/R/module-loadpage-ui.R index 4d00a9c..e9f9292 100644 --- a/R/module-loadpage-ui.R +++ b/R/module-loadpage-ui.R @@ -49,8 +49,15 @@ loadpageUI <- function(id) { # Processing options create_processing_options(ns), - # Action button - disabled(actionButton(inputId = ns("proceed1"), label = "Upload Data")) + # Action buttons + tags$div( + style = "display:flex; gap:8px; align-items:center;", + disabled(actionButton(inputId = ns("proceed1"), label = "Upload Data")), + shinyjs::disabled(downloadButton( + ns("download_msstats_format"), + "Download MSstats-format CSV" + )) + ) ), column(width = 8, diff --git a/R/module-qc-ui.R b/R/module-qc-ui.R index 39994b7..7a79a46 100644 --- a/R/module-qc-ui.R +++ b/R/module-qc-ui.R @@ -22,7 +22,7 @@ qcUI <- function(id) { tags$link(rel = "stylesheet", type = "text/css", href = "assets/style.css"), ), headerPanel("Process and quantify data"), - p("Feature summarization and missing value imputation. Includes options for vizualizing summarization through data tables and multiple plots. All outputs are available to download in 'csv' format."), + p("Feature summarization and missing value imputation. Includes options for vizualizing summarization through data tables and multiple plots. All outputs are available to download in 'csv' format. Imputation runs only when a feature is observed in some other run AND the protein has at least one observed feature in the current run."), tags$br(), sidebarPanel( # transformation @@ -141,7 +141,7 @@ qcUI <- function(id) { h4("Imputation"), conditionalPanel(condition = "input['qc-censInt'] == 'NA' || input['qc-censInt'] == '0'", checkboxInput(ns("MBi"), label=tags$div("Model based imputation",class = "icon-wrapper",icon("question-circle", lib = "font-awesome"), - div("If unchecked the values set as cutoff for censored will be used", class = "icon-tooltip")),value = TRUE + div("Fills in missing intensities only when (a) the protein has at least one observed feature in that run, AND (b) the missing feature is observed in at least one other run. Proteins entirely missing from a run, and features never observed in the dataset, are not imputed. If unchecked, the cutoff for censored values is used instead.", class = "icon-tooltip")),value = TRUE )), # # cutoff for censored # conditionalPanel(condition = "input.censInt == 'NA' || input.censInt == '0'", diff --git a/R/module-statmodel-server.R b/R/module-statmodel-server.R index 7efa844..0337d5d 100644 --- a/R/module-statmodel-server.R +++ b/R/module-statmodel-server.R @@ -53,6 +53,9 @@ statmodelServer = function(id, parent_session, loadpage_input, qc_input, choices = c("Turnover Curve" = CONSTANTS_STATMODEL$plot_type_response_curve), selected = CONSTANTS_STATMODEL$plot_type_response_curve) updateCheckboxInput(session, NAMESPACE_STATMODEL$modeling_response_curve_increasing_trend, value = TRUE) + shinyjs::hide("statmodel_contrast_header", asis = TRUE) + shinyjs::hide("statmodel_workflow_bullet_default", asis = TRUE) + shinyjs::show("statmodel_workflow_bullet_response_curve", asis = TRUE) } else if (template == TEMPLATES$chemoproteomics) { updateRadioButtons(session, NAMESPACE_STATMODEL$comparison_mode, choices = c("Create dose-response curves" = CONSTANTS_STATMODEL$comparison_mode_response_curve), @@ -61,6 +64,9 @@ statmodelServer = function(id, parent_session, loadpage_input, qc_input, choices = c("Dose-Response Curve" = CONSTANTS_STATMODEL$plot_type_response_curve), selected = CONSTANTS_STATMODEL$plot_type_response_curve) updateCheckboxInput(session, NAMESPACE_STATMODEL$modeling_response_curve_increasing_trend, value = FALSE) + shinyjs::hide("statmodel_contrast_header", asis = TRUE) + shinyjs::hide("statmodel_workflow_bullet_default", asis = TRUE) + shinyjs::show("statmodel_workflow_bullet_response_curve", asis = TRUE) } else { updateRadioButtons(session, NAMESPACE_STATMODEL$comparison_mode, choices = c( @@ -77,6 +83,9 @@ statmodelServer = function(id, parent_session, loadpage_input, qc_input, "Comparison Plot" = CONSTANTS_STATMODEL$plot_type_comparison_plot )) updateCheckboxInput(session, NAMESPACE_STATMODEL$modeling_response_curve_increasing_trend, value = FALSE) + shinyjs::show("statmodel_contrast_header", asis = TRUE) + shinyjs::show("statmodel_workflow_bullet_default", asis = TRUE) + shinyjs::hide("statmodel_workflow_bullet_response_curve", asis = TRUE) } }, ignoreInit = FALSE) diff --git a/R/statmodel-ui-comparisons.R b/R/statmodel-ui-comparisons.R index f89807a..c47167d 100644 --- a/R/statmodel-ui-comparisons.R +++ b/R/statmodel-ui-comparisons.R @@ -21,6 +21,7 @@ create_contrast_radio_buttons <- function(ns) { ns(NAMESPACE_STATMODEL$comparison_mode), label = h4( "1. Define comparisons - contrast matrix", + id = "statmodel_contrast_header", class = "icon-wrapper", icon("question-circle", lib = "font-awesome"), div("Define what conditions you want to compare here", class = "icon-tooltip") diff --git a/R/statmodel-ui-headers.R b/R/statmodel-ui-headers.R index a07d29d..0507cff 100644 --- a/R/statmodel-ui-headers.R +++ b/R/statmodel-ui-headers.R @@ -41,7 +41,12 @@ create_header_section <- function() { tagList( headerPanel("Statistical modeling and inference"), p("In this tab, build your statistical model in three steps:"), - p("(i) Create a contrast matrix for a group comparison or set up a configuration for a response curve analysis,"), + p(id = "statmodel_workflow_bullet_default", + "(i) Create a contrast matrix for a group comparison or set up a configuration for a response curve analysis,"), + shinyjs::hidden( + p(id = "statmodel_workflow_bullet_response_curve", + "(i) Configure the response curve analysis,") + ), p("(ii) generate the model and "), p("(iii) view result plots."), p("More info ", a("here", href="https://www.rdocumentation.org/packages/MSstats/versions/3.4.0/topics/groupComparisonPlots")) diff --git a/man/MSstatsShiny.Rd b/man/MSstatsShiny.Rd index 68c65a2..6f654ed 100644 --- a/man/MSstatsShiny.Rd +++ b/man/MSstatsShiny.Rd @@ -43,7 +43,6 @@ Useful links: Authors: \itemize{ - \item Anthony Wu \email{wu.anthon@northeastern.edu} \item Devon Kohler \email{kohler.d@northeastern.edu} \item Deril Raju \email{raju.d@northeastern.edu} \item Maanasa Kaza \email{maanasakaza@gmail.com} From 83c17d8f671d233e5b0b9cba5aa79e0c75939414 Mon Sep 17 00:00:00 2001 From: Swaraj Patil Date: Tue, 2 Jun 2026 16:11:30 -0400 Subject: [PATCH 2/6] Remove the unwanted file --- PREDESIGN.md | 123 --------------------------------------------------- 1 file changed, 123 deletions(-) delete mode 100644 PREDESIGN.md diff --git a/PREDESIGN.md b/PREDESIGN.md deleted file mode 100644 index 5009574..0000000 --- a/PREDESIGN.md +++ /dev/null @@ -1,123 +0,0 @@ -# MSstatsShiny scaling: cost breakdown + architecture options - -## 1. Current AWS spend - -Cost Explorer, 3-month average (Feb-Apr 2026), `olga-vitek-lab` account, `us-east-1`: - -| Service | Monthly avg | Notes | -|---|---|---| -| EC2-Instances | $27 | One `t3a.medium` running 24/7 (`i-0908994d2d2186f7a`, "nu-ov-lab-us-MSstatsShiny") | -| EC2-Other | $22 | EBS volumes + Elastic IP | -| VPC | $10 | Public IPv4 charges and/or VPC endpoints | -| S3 | $8-15 | Lab buckets; April spiked to $14.87 | -| Route 53 | $0.50 | Existing hosted zone for `msstatsshiny.com` | -| All others | $0 | CloudWatch, KMS, SNS, SQS - unused | -| **Total** | **~$70/month** | | - -One thing from the audit worth flagging: the existing CloudWatch `StatusCheckFailed` alarm targets a stale instance ID (`i-068bfdd11cb2703f2`), not the live production instance. The lab has no working monitoring on the site today. The new design fixes this as a side effect. - ---- - -## 2. Projected cost for the new architecture - -The new setup adds an ALB, Fargate, WAF, CloudWatch logs, and ECR. Both architecture options share the same fixed costs. - -### 2.1 Shared fixed costs - -| Component | Monthly | Notes | -|---|---|---| -| Application Load Balancer | ~$22 | $16.43 hourly + ~5 LCU | -| Route 53 | $0.50 | Existing zone | -| ACM certificate | $0 | Free, auto-renewing | -| ECR storage | $0.50 | ~5 GB of image versions | -| CloudWatch Logs | ~$5 | ~5 GB/month ingested | -| S3 (ALB access logs) | ~$1 | | -| AWS WAF | ~$8 | ACL + 3 managed rules + ~1M requests | -| **Subtotal** | **~$37/month** | | - -### 2.2 Variable cost: Fargate compute - -Sizing assumptions: -- **Option A**: ShinyProxy dispatcher always-on at 0.5 vCPU / 1 GB; user tasks spawned on demand at Fargate's floor of 0.25 vCPU / 0.5 GB. -- **Option B**: 2 tasks always-on at 1 vCPU / 2 GB for HA; autoscales up to 8 tasks under load. - -Usage pattern: site mostly idle, with one ~5-day short course per month (~50 concurrent users for ~8 hours/day). - -| Scenario | Option A | Option B | -|---|---|---| -| Idle month | ~$55/mo | ~$109/mo | -| Light steady traffic (~5 users) | ~$60/mo | ~$109/mo | -| Course month (one 5-day course) | ~$80/mo | ~$115/mo | -| Course month at hard cap (75 concurrent) | ~$95/mo | ~$125/mo | - -Option A is $30-50/month cheaper because user containers only run when users are active; Option B carries a ~$72/mo always-on baseline for its 2 pre-warmed tasks. - -**Caveat**: these numbers assume the sizing above. CloudWatch shows the current EC2 at 0.3% baseline CPU - dramatically over-provisioned. Local profiling (in progress) will likely let us shrink task sizes and cut costs further. - -**On the $150-200 framing**: when I floated $150-200 as a budget envelope earlier, that was an engineering-side guess - not a target the architecture forces. Realistic operating range is $80-125/month, so you can set a tighter ceiling if that fits the lab's grants better. - ---- - -## 3. Cost vs concurrent users - -Marginal cost of adding concurrent users (sustained for an 8-hour course session), on top of the baseline: - -| Concurrent users | Option A | Option B | -|---|---|---| -| 1 | +$2 | $0 (within baseline) | -| 5 | +$5 | $0 (within baseline) | -| 10 | +$10 | +$1 (one extra task) | -| 25 | +$25 | +$3 (one to two extra tasks) | -| 50 | +$50 | +$6 (three extra tasks) | -| 75 (hard cap) | +$74 | +$12 (six extra tasks at cap) | - -Option A scales linearly (each user = one task). Option B scales in steps (one task serves ~10 users, TBD with profiling). At 50 users, Option B is much cheaper per user; below ~10 users, Option A wins because nothing runs when nobody's active. - ---- - -## 4. ShinyProxy on Fargate: Option A vs Option B - -The key choice: do we keep ShinyProxy as a per-user dispatcher (current model, ported to Fargate), or replace it with multiple parallel Shiny tasks behind ALB sticky sessions? - -### 4.1 Option A - ShinyProxy on Fargate (one container per user) - -ShinyProxy runs as a long-lived Fargate task. When a user connects, it spawns a new Fargate task running the Shiny container, routes the user to it, and tears the task down when they disconnect. - -**Pros** -1. **Per-user isolation.** Each user gets their own container - one user's heavy computation, crash, or memory spike doesn't affect others. Per-user resource caps are possible. -2. **Lower idle cost.** Only the lightweight dispatcher runs 24/7; user containers consume nothing when nobody's active. -3. **Matches the current operational model.** Minimal mental shift for the team; the `application.yml` mental model carries over. - -**Cons** -1. **Cold-start latency.** Fargate task spin-up is 30-60 seconds. Every new user waits on a loading screen on first hit - bad UX for a demo site where first-time visitors are the whole point. -2. **~3× the CDK complexity.** Needs a ShinyProxy task definition, IAM role for dynamic ECS task launches, networking for spawned tasks, plus ongoing `application.yml` management. -3. **Open-source dispatcher, no AWS support.** Integration issues between ShinyProxy and the Fargate ECS backend are on us to debug. -4. **JVM and ECR overhead.** ShinyProxy adds 500 MB - 1 GB memory baseline regardless of users; every spawned session pulls the image. - -### 4.2 Option B - Multi-task Shiny service with ALB sticky sessions - -Multiple Shiny Fargate tasks run in parallel, pre-warmed. The ALB uses cookie-based stickiness to bind each user to one task for their session. ECS autoscales task count on CPU and request volume. - -**Pros** -1. **No cold-start.** Tasks are pre-warmed; users get instant page loads. -2. **Simpler overall.** Standard ECS service pattern, ~1/3 the CDK code of Option A, standard CloudWatch metrics, no dispatcher single-point-of-failure. -3. **Native autoscaling.** ECS scales on CPU / request count; no custom dispatcher logic. - -**Cons** -1. **Shared R process per task.** Multiple users share one task's resources, with no per-user caps. A heavy computation slows others on the same task; a crash impacts all ~10 users on that task. -2. **Sticky session fragility.** Users who clear cookies or switch networks mid-session may be re-routed and lose state. -3. **Higher idle cost.** 2 tasks run 24/7 for HA even with zero users. -4. **Capacity planning needs profiling.** "Users per task" is determined empirically. - ---- - -## 5. Two decisions requested - -**Decision 1 - Architecture: Option A or Option B?** - -Both fit comfortably under recommended budget. The trade-off is cold-start UX (Option A's biggest con) vs per-user isolation and lower cost (Option A's pros). - -**Decision 2 - Cost ceiling.** Three reasonable framings, all with hard caps and AWS Budgets alerts to prevent runaway spend: -- (a) **~$125/month** - tight ceiling, optimizes for cost -- (b) **~$150/month** - comfortable for either option -- (c) **~$200/month** - generous headroom for future growth \ No newline at end of file From ba470f0fdb146b2b2fb8863d3ad683d506f2e53b Mon Sep 17 00:00:00 2001 From: Swaraj Patil Date: Tue, 2 Jun 2026 16:19:49 -0400 Subject: [PATCH 3/6] Revert unintended doc regeneration (RoxygenNote, MSstatsShiny.Rd) --- DESCRIPTION | 1 - man/MSstatsShiny.Rd | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 3637c4d..b1b8d96 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -44,4 +44,3 @@ Encoding: UTF-8 Roxygen: list(markdown = TRUE) Config/testthat/edition: 3 Config/roxygen2/version: 8.0.0 -RoxygenNote: 7.3.3 diff --git a/man/MSstatsShiny.Rd b/man/MSstatsShiny.Rd index 6f654ed..68c65a2 100644 --- a/man/MSstatsShiny.Rd +++ b/man/MSstatsShiny.Rd @@ -43,6 +43,7 @@ Useful links: Authors: \itemize{ + \item Anthony Wu \email{wu.anthon@northeastern.edu} \item Devon Kohler \email{kohler.d@northeastern.edu} \item Deril Raju \email{raju.d@northeastern.edu} \item Maanasa Kaza \email{maanasakaza@gmail.com} From 1551351c3f1e6643b63e2b1609a0a7198a666eca Mon Sep 17 00:00:00 2001 From: Swaraj Patil Date: Tue, 2 Jun 2026 16:35:40 -0400 Subject: [PATCH 4/6] Resolve coderabbit-ai issues --- DESCRIPTION | 1 + R/module-loadpage-server.R | 30 ++++++++++++++++++++++++++++-- R/module-qc-ui.R | 2 +- R/statmodel-ui-headers.R | 2 +- man/MSstatsShiny.Rd | 1 - 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index b1b8d96..3637c4d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -44,3 +44,4 @@ Encoding: UTF-8 Roxygen: list(markdown = TRUE) Config/testthat/edition: 3 Config/roxygen2/version: 8.0.0 +RoxygenNote: 7.3.3 diff --git a/R/module-loadpage-server.R b/R/module-loadpage-server.R index 5f284b1..b73ae53 100644 --- a/R/module-loadpage-server.R +++ b/R/module-loadpage-server.R @@ -563,6 +563,10 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE, app_templa ) }) + observeEvent(input$proceed1, { + shinyjs::disable("download_msstats_format") + }) + observeEvent(get_data(), { req(get_data()) shinyjs::enable("download_msstats_format") @@ -570,10 +574,32 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE, app_templa output$download_msstats_format = downloadHandler( filename = function() { - paste0("MSstats_format-", Sys.Date(), ".csv") + data <- get_data() + if (inherits(data, "data.frame")) { + paste0("MSstats_format-", Sys.Date(), ".csv") + } else { + paste0("MSstats_format-", Sys.Date(), ".zip") + } }, content = function(file) { - data.table::fwrite(get_data(), file) + data <- get_data() + if (inherits(data, "data.frame")) { + data.table::fwrite(data, file) + } else { + tmp_dir <- tempfile("msstats_format_") + dir.create(tmp_dir) + on.exit(unlink(tmp_dir, recursive = TRUE), add = TRUE) + tmp_files <- character() + for (nm in names(data)) { + tbl <- data[[nm]] + if (is.null(tbl)) next + if (NROW(tbl) == 0L) next + tmp_path <- file.path(tmp_dir, paste0(nm, ".csv")) + data.table::fwrite(tbl, tmp_path) + tmp_files <- c(tmp_files, tmp_path) + } + utils::zip(zipfile = file, files = tmp_files, flags = "-j") + } } ) diff --git a/R/module-qc-ui.R b/R/module-qc-ui.R index 7a79a46..f729573 100644 --- a/R/module-qc-ui.R +++ b/R/module-qc-ui.R @@ -22,7 +22,7 @@ qcUI <- function(id) { tags$link(rel = "stylesheet", type = "text/css", href = "assets/style.css"), ), headerPanel("Process and quantify data"), - p("Feature summarization and missing value imputation. Includes options for vizualizing summarization through data tables and multiple plots. All outputs are available to download in 'csv' format. Imputation runs only when a feature is observed in some other run AND the protein has at least one observed feature in the current run."), + p("Feature summarization and missing value imputation. Includes options for visualizing summarization through data tables and multiple plots. Summarized tables and processed datasets are available to download in CSV format. Imputation runs only when a feature is observed in some other run AND the protein has at least one observed feature in the current run."), tags$br(), sidebarPanel( # transformation diff --git a/R/statmodel-ui-headers.R b/R/statmodel-ui-headers.R index 0507cff..8e866c1 100644 --- a/R/statmodel-ui-headers.R +++ b/R/statmodel-ui-headers.R @@ -42,7 +42,7 @@ create_header_section <- function() { headerPanel("Statistical modeling and inference"), p("In this tab, build your statistical model in three steps:"), p(id = "statmodel_workflow_bullet_default", - "(i) Create a contrast matrix for a group comparison or set up a configuration for a response curve analysis,"), + "(i) Create a contrast matrix for a group comparison,"), shinyjs::hidden( p(id = "statmodel_workflow_bullet_response_curve", "(i) Configure the response curve analysis,") diff --git a/man/MSstatsShiny.Rd b/man/MSstatsShiny.Rd index 68c65a2..6f654ed 100644 --- a/man/MSstatsShiny.Rd +++ b/man/MSstatsShiny.Rd @@ -43,7 +43,6 @@ Useful links: Authors: \itemize{ - \item Anthony Wu \email{wu.anthon@northeastern.edu} \item Devon Kohler \email{kohler.d@northeastern.edu} \item Deril Raju \email{raju.d@northeastern.edu} \item Maanasa Kaza \email{maanasakaza@gmail.com} From d35c11623fd62a6d568e70ca3bec3e2640c79927 Mon Sep 17 00:00:00 2001 From: Swaraj Patil Date: Tue, 2 Jun 2026 21:05:31 -0400 Subject: [PATCH 5/6] Revert the unnecessary changes of DESCRIPTION and documentation --- DESCRIPTION | 1 - man/MSstatsShiny.Rd | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 3637c4d..b1b8d96 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -44,4 +44,3 @@ Encoding: UTF-8 Roxygen: list(markdown = TRUE) Config/testthat/edition: 3 Config/roxygen2/version: 8.0.0 -RoxygenNote: 7.3.3 diff --git a/man/MSstatsShiny.Rd b/man/MSstatsShiny.Rd index 6f654ed..68c65a2 100644 --- a/man/MSstatsShiny.Rd +++ b/man/MSstatsShiny.Rd @@ -43,6 +43,7 @@ Useful links: Authors: \itemize{ + \item Anthony Wu \email{wu.anthon@northeastern.edu} \item Devon Kohler \email{kohler.d@northeastern.edu} \item Deril Raju \email{raju.d@northeastern.edu} \item Maanasa Kaza \email{maanasakaza@gmail.com} From 78a2756117838d6df2680c2c911a5348255e4b33 Mon Sep 17 00:00:00 2001 From: Swaraj Patil Date: Tue, 2 Jun 2026 21:22:39 -0400 Subject: [PATCH 6/6] Address coderabbit-ai review to handle content function body by wrapping in a trycatch block --- R/module-loadpage-server.R | 41 ++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/R/module-loadpage-server.R b/R/module-loadpage-server.R index b73ae53..4c00602 100644 --- a/R/module-loadpage-server.R +++ b/R/module-loadpage-server.R @@ -582,24 +582,31 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE, app_templa } }, content = function(file) { - data <- get_data() - if (inherits(data, "data.frame")) { - data.table::fwrite(data, file) - } else { - tmp_dir <- tempfile("msstats_format_") - dir.create(tmp_dir) - on.exit(unlink(tmp_dir, recursive = TRUE), add = TRUE) - tmp_files <- character() - for (nm in names(data)) { - tbl <- data[[nm]] - if (is.null(tbl)) next - if (NROW(tbl) == 0L) next - tmp_path <- file.path(tmp_dir, paste0(nm, ".csv")) - data.table::fwrite(tbl, tmp_path) - tmp_files <- c(tmp_files, tmp_path) + tryCatch({ + data <- get_data() + if (inherits(data, "data.frame")) { + data.table::fwrite(data, file) + } else { + tmp_dir <- tempfile("msstats_format_") + dir.create(tmp_dir) + on.exit(unlink(tmp_dir, recursive = TRUE), add = TRUE) + tmp_files <- character() + for (nm in names(data)) { + tbl <- data[[nm]] + if (is.null(tbl)) next + if (NROW(tbl) == 0L) next + tmp_path <- file.path(tmp_dir, paste0(nm, ".csv")) + data.table::fwrite(tbl, tmp_path) + tmp_files <- c(tmp_files, tmp_path) + } + if (length(tmp_files) == 0L) { + stop("No non-empty tables available to export.") + } + utils::zip(zipfile = file, files = tmp_files, flags = "-j") } - utils::zip(zipfile = file, files = tmp_files, flags = "-j") - } + }, error = function(e) { + writeLines(paste("Failed to export MSstats format:", conditionMessage(e)), file) + }) } )