diff --git a/inst/shiny/functions/utils-exclusions.R b/inst/shiny/functions/utils-exclusions.R index 95c543eca..c2cdf22e4 100644 --- a/inst/shiny/functions/utils-exclusions.R +++ b/inst/shiny/functions/utils-exclusions.R @@ -90,6 +90,12 @@ EXCL_COLOR_BOTH <- "#FFD9B3" # orange — NCA + TLG exclusion_list(append(current, list_new_reason)) updateTextInput(session, "exclusion_reason", value = "") updateReactable(table_id, selected = NA) + + type_label <- .exclusion_type_label(nca_checked, tlg_checked) + log_info( + "Exclusion added: ", length(rows_sel), " rows, type=", type_label, + ", reason='", reason, "'" + ) } } diff --git a/inst/shiny/modules/tab_nca/setup/data_imputation.R b/inst/shiny/modules/tab_nca/setup/data_imputation.R index eb5c5f6f9..a793c08ba 100644 --- a/inst/shiny/modules/tab_nca/setup/data_imputation.R +++ b/inst/shiny/modules/tab_nca/setup/data_imputation.R @@ -196,6 +196,15 @@ data_imputation_server <- function(id, settings_override) { } }) + observeEvent(input$select_blq_strategy, { + log_info("BLQ imputation strategy changed: ", input$select_blq_strategy) + }, ignoreInit = TRUE) + + observeEvent(input$should_impute_c0, { + state <- if (isTRUE(input$should_impute_c0)) "enabled" else "disabled" + log_info("Start concentration imputation ", state) + }, ignoreInit = TRUE) + blq_imputation_rule <- reactive({ req(input$select_blq_strategy) rule_list <- switch(input$select_blq_strategy, diff --git a/inst/shiny/modules/tab_nca/setup/general_exclusions.R b/inst/shiny/modules/tab_nca/setup/general_exclusions.R index 157258962..4fe331d3e 100644 --- a/inst/shiny/modules/tab_nca/setup/general_exclusions.R +++ b/inst/shiny/modules/tab_nca/setup/general_exclusions.R @@ -131,6 +131,7 @@ general_exclusions_server <- function( xbtn_counter(max(new_ids)) exclusion_list(rehydrated_list) + log_info("Exclusions restored from settings: ", length(overrides), " rules loaded") } }) @@ -167,6 +168,7 @@ general_exclusions_server <- function( lapply(lst, function(item) { xbtn_id <- item$xbtn_id observeEvent(input[[xbtn_id]], { + log_info("Exclusion removed: reason='", item$reason, "'") current <- exclusion_list() exclusion_list( Filter(function(x) x$xbtn_id != xbtn_id, current) diff --git a/inst/shiny/modules/tab_nca/setup/manual_slopes_table.R b/inst/shiny/modules/tab_nca/setup/manual_slopes_table.R index 5475c0466..b7f0c8f1a 100644 --- a/inst/shiny/modules/tab_nca/setup/manual_slopes_table.R +++ b/inst/shiny/modules/tab_nca/setup/manual_slopes_table.R @@ -93,7 +93,7 @@ manual_slopes_table_server <- function( # Add a new row to the table when the user clicks the add button observeEvent(input$add_rule, { - log_trace("{id}: adding manual slopes row") + log_trace("Slope selector: adding manual slope rule") first_group <- slopes_pknca_groups()[1, ] time_col <- pknca_data()$conc$columns$time new_row <- cbind( @@ -126,7 +126,7 @@ manual_slopes_table_server <- function( # Remove selected rows from the table when the user clicks the remove button observeEvent(input$remove_rule, { - log_trace("{id}: removing manual slopes row") + log_trace("Slope selector: removing manual slope rule") selected <- getReactableState("manual_slopes", "selected") req(selected) edited_slopes <- manual_slopes()[-selected, ] @@ -138,7 +138,7 @@ manual_slopes_table_server <- function( # Render the manual slopes table (reactable) output$manual_slopes <- renderReactable({ req(manual_slopes()) - log_trace("{id}: rendering slope edit data table") + log_trace(id, ": rendering slope edit data table") isolate({ data <- manual_slopes() }) diff --git a/inst/shiny/modules/tab_nca/setup/parameter_selection.R b/inst/shiny/modules/tab_nca/setup/parameter_selection.R index 5d5e860fe..994b91908 100644 --- a/inst/shiny/modules/tab_nca/setup/parameter_selection.R +++ b/inst/shiny/modules/tab_nca/setup/parameter_selection.R @@ -286,7 +286,10 @@ parameter_selection_server <- function(id, processed_pknca_data, parameter_overr params <- selection_debounce() n_params <- if (is.null(params)) 0 else length(params) - log_info("Parameter selection for '{study_type}': {n_params} parameters selected.") + log_info("Parameter selection for '", study_type, "': ", n_params, " parameters selected.") + if (n_params > 0) { + log_debug("Parameters for '", study_type, "': ", paste(params, collapse = ", ")) + } }, ignoreNULL = FALSE, ignoreInit = TRUE) }) }) diff --git a/inst/shiny/modules/tab_nca/setup/settings.R b/inst/shiny/modules/tab_nca/setup/settings.R index b8818a9f4..a6fe79ba4 100644 --- a/inst/shiny/modules/tab_nca/setup/settings.R +++ b/inst/shiny/modules/tab_nca/setup/settings.R @@ -258,6 +258,10 @@ settings_server <- function(id, data, adnca_data, settings_override) { # A guard flag prevents infinite observer loops. updating_filters <- reactiveVal(FALSE) + # Guard to suppress log noise during initial data load / settings restore. + # Set to TRUE after the first analyte cascade completes. + filters_initialized <- reactiveVal(FALSE) + # settings_override is consumed once during the first cascade after # settings upload. After that, pending_settings is set to NULL so # subsequent user-driven changes are not overridden by stale values. @@ -299,7 +303,14 @@ settings_server <- function(id, data, adnca_data, settings_override) { req(data(), input$select_analyte) if (updating_filters()) return() updating_filters(TRUE) - on.exit(updating_filters(FALSE)) + on.exit({ + updating_filters(FALSE) + if (!filters_initialized()) filters_initialized(TRUE) + }) + + if (filters_initialized()) { + log_info("Analyte selection changed: ", paste(input$select_analyte, collapse = ", ")) + } settings <- .consume_settings() @@ -335,6 +346,10 @@ settings_server <- function(id, data, adnca_data, settings_override) { updating_filters(TRUE) on.exit(updating_filters(FALSE)) + if (filters_initialized()) { + log_info("Specimen selection changed: ", paste(input$select_pcspec, collapse = ", ")) + } + settings <- .consume_settings() all_analyte <- unique(data()$PARAM) %>% na.omit() @@ -356,6 +371,25 @@ settings_server <- function(id, data, adnca_data, settings_override) { .update_profile(settings) }) + # Log profile, method, and min half-life points changes + observeEvent(input$select_profile, { + if (filters_initialized()) { + log_info("NCA profile changed: ", paste(input$select_profile, collapse = ", ")) + } + }, ignoreInit = TRUE) + + observeEvent(input$method, { + if (filters_initialized()) { + log_info("Extrapolation method changed: ", input$method) + } + }, ignoreInit = TRUE) + + observeEvent(input$min_hl_points, { + if (filters_initialized()) { + log_info("Min. half-life points changed: ", input$min_hl_points) + } + }, ignoreInit = TRUE) + # Include keyboard limits for the settings GUI display # Keyboard limits for the setting thresholds @@ -365,6 +399,19 @@ settings_server <- function(id, data, adnca_data, settings_override) { limit_input_value(input, session, "LAMZSPN_threshold", min = 0, lab = "LAMZSPN") limit_input_value(input, session, "min_hl_points", max = 10, min = 2, lab = "Min. HL Points") + # Log flag rule changes + lapply(c("R2ADJ", "R2", "AUCPEO", "AUCPEP", "LAMZSPN"), function(flag) { + rule_id <- paste0(flag, "_rule") + threshold_id <- paste0(flag, "_threshold") + observeEvent(input[[rule_id]], { + state <- if (input[[rule_id]]) "enabled" else "disabled" + log_info("Flag rule ", flag, " ", state) + }, ignoreInit = TRUE) + observeEvent(input[[threshold_id]], { + log_info("Flag rule ", flag, " threshold changed: ", input[[threshold_id]]) + }, ignoreInit = TRUE) + }) + # Reactive value to store the partial intervals data table # Define the parameters that can be used for partial area calculations PARTIAL_INT_PARAMS <- metadata_nca_parameters %>% diff --git a/inst/shiny/modules/tab_nca/setup/slope_selector.R b/inst/shiny/modules/tab_nca/setup/slope_selector.R index 7f3cfe0f1..5c9f2a05c 100644 --- a/inst/shiny/modules/tab_nca/setup/slope_selector.R +++ b/inst/shiny/modules/tab_nca/setup/slope_selector.R @@ -246,6 +246,9 @@ slope_selector_server <- function( # nolint manual_slopes <- slopes_table$manual_slopes refresh_reactable <- slopes_table$refresh_reactable + # Track user-initiated slope changes (buttons, plotly clicks) + user_changed_slopes <- reactiveVal(FALSE) + # Define the click events for the point exclusion and selection in the slope plots last_click_data <- reactiveVal(NULL) observeEvent(event_data("plotly_click", priority = "event"), { @@ -259,6 +262,7 @@ slope_selector_server <- function( # nolint ) # Update reactive values: last click & manual slopes table last_click_data(click_result$last_click_data) + user_changed_slopes(TRUE) manual_slopes(click_result$manual_slopes) # render rectable anew # @@ -266,12 +270,25 @@ slope_selector_server <- function( # nolint refresh_reactable(refresh_reactable() + 1) }) + # Also flag user-initiated changes from add/remove buttons + observeEvent(slopes_table$refresh_reactable(), { + user_changed_slopes(TRUE) + }, ignoreInit = TRUE) + #' Separate event handling updating displayed reactable upon every change (adding and removing #' rows, plots selection, edits). This needs to be separate call, since simply re-rendering #' the table would mean losing focus on text inputs when entering values. observeEvent(manual_slopes(), { req(manual_slopes()) + if (user_changed_slopes()) { + n_rules <- nrow(manual_slopes()) + n_excl <- sum(manual_slopes()$TYPE == "Exclusion", na.rm = TRUE) + n_incl <- n_rules - n_excl + log_info("Slope rules updated: ", n_rules, " total (", n_incl, " selections, ", n_excl, " exclusions)") + user_changed_slopes(FALSE) + } + # Update reactable with rules reactable::updateReactable( outputId = "manual_slopes",