Skip to content

callbacks

Callbacks Package - BioRemPP v1.0.

Dash callbacks for UI interactivity and backend integration.

Functions

register_info_modal_callbacks

register_info_modal_callbacks(app)

Register info modal callbacks with Dash app.

Parameters:

Name Type Description Default
app Dash

Dash application instance

required
Notes

Registered Callbacks: 1. Toggle modal (open button, close button, backdrop click)

Behavior: - Modal opens when "More Info" button is clicked - Modal closes when X button is clicked or when clicking outside - Modal is extra-large and vertically centered

See Also

create_info_modal : Modal component create_intro_card : Intro card with More Info button

Source code in src/presentation/callbacks/info_modal_callbacks.py
def register_info_modal_callbacks(app):
    """
    Register info modal callbacks with Dash app.

    Parameters
    ----------
    app : Dash
        Dash application instance

    Notes
    -----
    Registered Callbacks:
    1. Toggle modal (open button, close button, backdrop click)

    Behavior:
    - Modal opens when "More Info" button is clicked
    - Modal closes when X button is clicked or when clicking outside
    - Modal is extra-large and vertically centered

    See Also
    --------
    create_info_modal : Modal component
    create_intro_card : Intro card with More Info button
    """
    global _info_modal_callbacks_registered

    # Prevent duplicate registration
    if _info_modal_callbacks_registered:
        logger.warning(
            "[INFO_MODAL] Callbacks already registered, skipping duplicate registration"
        )
        return

    logger.info("[INFO_MODAL] Registering info modal callbacks...")

    # ========================================
    # Callback: Toggle Modal Open/Close
    # ========================================
    @app.callback(
        Output("info-modal", "is_open"),
        [
            Input("info-modal-open-button", "n_clicks"),
            Input("info-modal-close-button", "n_clicks"),
        ],
        State("info-modal", "is_open"),
        prevent_initial_call=True,
    )
    def toggle_info_modal(open_clicks, close_clicks, is_open):
        """
        Toggle info modal open/close state.

        Parameters
        ----------
        open_clicks : int
            Number of open button clicks
        close_clicks : int
            Number of close button clicks
        is_open : bool
            Current modal state

        Returns
        -------
        bool
            New modal state (toggled based on which button was clicked)

        Notes
        -----
        - Uses ctx.triggered_id to determine which button was clicked
        - Open button opens the modal
        - Close button closes the modal
        - Backdrop click automatically closes (handled by dbc.Modal backdrop=True)
        """
        logger.info("=" * 80)
        logger.info(f"[INFO_MODAL-{_callback_instance_id}] TOGGLE_INFO_MODAL CALLED")
        logger.info(
            f"[INFO_MODAL-{_callback_instance_id}]   Input open_clicks: {open_clicks}"
        )
        logger.info(
            f"[INFO_MODAL-{_callback_instance_id}]   Input close_clicks: {close_clicks}"
        )
        logger.info(f"[INFO_MODAL-{_callback_instance_id}]   State is_open: {is_open}")
        logger.info(
            f"[INFO_MODAL-{_callback_instance_id}]   ctx.triggered: {ctx.triggered}"
        )
        logger.info(
            f"[INFO_MODAL-{_callback_instance_id}]   ctx.triggered_id: {ctx.triggered_id}"
        )

        if not ctx.triggered_id:
            logger.warning(
                f"[INFO_MODAL-{_callback_instance_id}] No triggered_id, returning no_update"
            )
            logger.info("=" * 80)
            return no_update

        # Determine action based on which button was clicked
        if ctx.triggered_id == "info-modal-open-button":
            new_state = True
            logger.info(
                f"[INFO_MODAL-{_callback_instance_id}] OPEN BUTTON CLICKED: "
                f"Setting modal to OPEN (clicks={open_clicks})"
            )
        elif ctx.triggered_id == "info-modal-close-button":
            new_state = False
            logger.info(
                f"[INFO_MODAL-{_callback_instance_id}] CLOSE BUTTON CLICKED: "
                f"Setting modal to CLOSED (clicks={close_clicks})"
            )
        else:
            logger.warning(
                f"[INFO_MODAL-{_callback_instance_id}] Unknown trigger: {ctx.triggered_id}, "
                f"returning current state"
            )
            new_state = is_open

        logger.info(f"[INFO_MODAL-{_callback_instance_id}] RETURNING: {new_state}")
        logger.info("=" * 80)
        return new_state

    # ========================================
    # Callback: Toggle Sample Data Modal Open/Close
    # ========================================
    @app.callback(
        Output("sample-data-modal", "is_open"),
        [
            Input("sample-data-card", "n_clicks"),
            Input("sample-data-modal-close-button", "n_clicks"),
        ],
        State("sample-data-modal", "is_open"),
        prevent_initial_call=True,
    )
    def toggle_sample_data_modal(card_clicks, close_clicks, is_open):
        """
        Toggle sample data modal open/close state.

        Parameters
        ----------
        card_clicks : int
            Number of card clicks
        close_clicks : int
            Number of close button clicks
        is_open : bool
            Current modal state

        Returns
        -------
        bool
            New modal state (toggled based on which element was clicked)

        Notes
        -----
        - Uses ctx.triggered_id to determine which element was clicked
        - Card click opens the modal
        - Close button closes the modal
        - Backdrop click automatically closes (handled by dbc.Modal backdrop=True)
        """
        logger.info("=" * 80)
        logger.info(
            f"[SAMPLE_DATA_MODAL-{_callback_instance_id}] TOGGLE_SAMPLE_DATA_MODAL CALLED"
        )
        logger.info(
            f"[SAMPLE_DATA_MODAL-{_callback_instance_id}]   Input card_clicks: {card_clicks}"
        )
        logger.info(
            f"[SAMPLE_DATA_MODAL-{_callback_instance_id}]   Input close_clicks: {close_clicks}"
        )
        logger.info(
            f"[SAMPLE_DATA_MODAL-{_callback_instance_id}]   State is_open: {is_open}"
        )
        logger.info(
            f"[SAMPLE_DATA_MODAL-{_callback_instance_id}]   ctx.triggered: {ctx.triggered}"
        )
        logger.info(
            f"[SAMPLE_DATA_MODAL-{_callback_instance_id}]   ctx.triggered_id: {ctx.triggered_id}"
        )

        if not ctx.triggered_id:
            logger.warning(
                f"[SAMPLE_DATA_MODAL-{_callback_instance_id}] No triggered_id, returning no_update"
            )
            logger.info("=" * 80)
            return no_update

        # Determine action based on which element was clicked
        if ctx.triggered_id == "sample-data-card":
            new_state = True
            logger.info(
                f"[SAMPLE_DATA_MODAL-{_callback_instance_id}] CARD CLICKED: "
                f"Setting modal to OPEN (clicks={card_clicks})"
            )
        elif ctx.triggered_id == "sample-data-modal-close-button":
            new_state = False
            logger.info(
                f"[SAMPLE_DATA_MODAL-{_callback_instance_id}] CLOSE BUTTON CLICKED: "
                f"Setting modal to CLOSED (clicks={close_clicks})"
            )
        else:
            logger.warning(
                f"[SAMPLE_DATA_MODAL-{_callback_instance_id}] Unknown trigger: {ctx.triggered_id}, "
                f"returning current state"
            )
            new_state = is_open

        logger.info(
            f"[SAMPLE_DATA_MODAL-{_callback_instance_id}] RETURNING: {new_state}"
        )
        logger.info("=" * 80)
        return new_state

    # ========================================
    # Callback: Trigger sample data download
    # ========================================
    @app.callback(
        Output("sample-data-download", "data"),
        Input("sample-data-download-btn", "n_clicks"),
        prevent_initial_call=True,
    )
    def download_sample_data(n_clicks):
        """Download sample dataset file without URL navigation."""
        if not n_clicks:
            return no_update

        dataset_path = (
            Path(__file__).resolve().parents[3] / "data" / "exemple_dataset.txt"
        )
        if not dataset_path.exists():
            logger.error("[SAMPLE_DATA_MODAL] Sample dataset file not found")
            return no_update

        return dcc.send_file(str(dataset_path), "exemple_dataset.txt")

    # ========================================
    # Callback: Toggle Publications Modal Open/Close
    # ========================================
    @app.callback(
        Output("publications-modal", "is_open"),
        [
            Input("publications-card", "n_clicks"),
            Input("publications-modal-close-button", "n_clicks"),
        ],
        State("publications-modal", "is_open"),
        prevent_initial_call=True,
    )
    def toggle_publications_modal(card_clicks, close_clicks, is_open):
        """
        Toggle publications modal open/close state.

        Parameters
        ----------
        card_clicks : int
            Number of card clicks
        close_clicks : int
            Number of close button clicks
        is_open : bool
            Current modal state

        Returns
        -------
        bool
            New modal state (toggled based on which element was clicked)

        Notes
        -----
        - Uses ctx.triggered_id to determine which element was clicked
        - Card click opens the modal
        - Close button closes the modal
        - Backdrop click automatically closes (handled by dbc.Modal backdrop=True)
        """
        logger.info("=" * 80)
        logger.info(
            f"[PUBLICATIONS_MODAL-{_callback_instance_id}] TOGGLE_PUBLICATIONS_MODAL CALLED"
        )
        logger.info(
            f"[PUBLICATIONS_MODAL-{_callback_instance_id}]   Input card_clicks: {card_clicks}"
        )
        logger.info(
            f"[PUBLICATIONS_MODAL-{_callback_instance_id}]   Input close_clicks: {close_clicks}"
        )
        logger.info(
            f"[PUBLICATIONS_MODAL-{_callback_instance_id}]   State is_open: {is_open}"
        )
        logger.info(
            f"[PUBLICATIONS_MODAL-{_callback_instance_id}]   ctx.triggered: {ctx.triggered}"
        )
        logger.info(
            f"[PUBLICATIONS_MODAL-{_callback_instance_id}]   ctx.triggered_id: {ctx.triggered_id}"
        )

        if not ctx.triggered_id:
            logger.warning(
                f"[PUBLICATIONS_MODAL-{_callback_instance_id}] No triggered_id, returning no_update"
            )
            logger.info("=" * 80)
            return no_update

        # Determine action based on which element was clicked
        if ctx.triggered_id == "publications-card":
            new_state = True
            logger.info(
                f"[PUBLICATIONS_MODAL-{_callback_instance_id}] CARD CLICKED: "
                f"Setting modal to OPEN (clicks={card_clicks})"
            )
        elif ctx.triggered_id == "publications-modal-close-button":
            new_state = False
            logger.info(
                f"[PUBLICATIONS_MODAL-{_callback_instance_id}] CLOSE BUTTON CLICKED: "
                f"Setting modal to CLOSED (clicks={close_clicks})"
            )
        else:
            logger.warning(
                f"[PUBLICATIONS_MODAL-{_callback_instance_id}] Unknown trigger: {ctx.triggered_id}, "
                f"returning current state"
            )
            new_state = is_open

        logger.info(
            f"[PUBLICATIONS_MODAL-{_callback_instance_id}] RETURNING: {new_state}"
        )
        logger.info("=" * 80)
        return new_state

    # Mark callbacks as registered
    _info_modal_callbacks_registered = True
    logger.info("[INFO_MODAL] [OK] Info modal callbacks registered successfully")
    logger.info(f"[INFO_MODAL] [OK] Callback IDs registered:")
    logger.info(
        f"[INFO_MODAL]     - toggle_info_modal: Inputs=['info-modal-open-button', 'info-modal-close-button'], Output='info-modal.is_open'"
    )
    logger.info(
        f"[INFO_MODAL]     - toggle_sample_data_modal: Inputs=['sample-data-card', 'sample-data-modal-close-button'], Output='sample-data-modal.is_open'"
    )
    logger.info(
        "[INFO_MODAL]     - download_sample_data: Input='sample-data-download-btn', Output='sample-data-download.data'"
    )
    logger.info(
        f"[INFO_MODAL]     - toggle_publications_modal: Inputs=['publications-card', 'publications-modal-close-button'], Output='publications-modal.is_open'"
    )

register_job_resume_callbacks

register_job_resume_callbacks(app)

Register callbacks related to resume-by-job-id flow.

Source code in src/presentation/callbacks/job_resume_callbacks.py
def register_job_resume_callbacks(app):
    """Register callbacks related to resume-by-job-id flow."""
    logger.info("=" * 60)
    logger.info("Registering JOB RESUME callbacks...")
    logger.info("=" * 60)

    @app.callback(
        Output("resume-browser-token-store", "data"),
        Input("resume-browser-token-store", "modified_timestamp"),
        State("resume-browser-token-store", "data"),
    )
    @instrument_callback("resume.ensure_browser_token")
    def ensure_resume_browser_token(_, existing_token):
        token = initialize_resume_browser_token(existing_token)
        if token is no_update:
            raise PreventUpdate
        logger.info("Resume browser token initialized")
        return token

    @app.callback(
        [
            Output("merged-result-store", "data", allow_duplicate=True),
            Output("results-context-store", "data", allow_duplicate=True),
            Output("url", "pathname"),
            Output("url", "hash"),
            Output("resume-job-status", "children"),
        ],
        Input("resume-job-btn", "n_clicks"),
        [
            State("resume-job-id-input", "value"),
            State("resume-browser-token-store", "data"),
        ],
        prevent_initial_call=True,
    )
    @instrument_callback("resume.resume_job_by_id")
    def resume_job_by_id(n_clicks, job_id, owner_token):
        if n_clicks is None:
            raise PreventUpdate

        return resolve_resume_request(job_id, owner_token)

    logger.info("[OK] Job resume callbacks registered successfully")

register_module1_callbacks

register_module1_callbacks(app, plot_service) -> None

Register all Module 1 callbacks with Dash app.

Parameters:

Name Type Description Default
app Dash

Dash application instance.

required
plot_service PlotService

Singleton PlotService instance (shared across all callbacks).

required
Notes
  • Registers callback handlers for Module 1 use cases
  • Refer to official documentation for supported use case details
Source code in src/presentation/callbacks/module_callbacks/module1_callbacks.py
def register_module1_callbacks(app, plot_service) -> None:
    """
    Register all Module 1 callbacks with Dash app.

    Parameters
    ----------
    app : Dash
        Dash application instance.
    plot_service : PlotService
        Singleton PlotService instance (shared across all callbacks).

    Notes
    -----
    - Registers callback handlers for Module 1 use cases
    - Refer to official documentation for supported use case details
    """
    logger.info("Starting Module 1 callback registration")

    try:
        # UC-1.1: Database Overlap and Unique Contributions
        logger.debug("Registering UC-1.1 callbacks...")
        register_uc_1_1_callbacks(app, plot_service)
        logger.info("[OK] UC-1.1 callbacks registered successfully")

        # UC-1.2: Regulatory Agency Compound Overlap
        logger.debug("Registering UC-1.2 callbacks...")
        register_uc_1_2_callbacks(app, plot_service)
        logger.info("[OK] UC-1.2 callbacks registered successfully")

        # UC-1.3: Proportional Contribution of Reference Agencies
        logger.debug("Registering UC-1.3 callbacks...")
        register_uc_1_3_callbacks(app, plot_service)
        logger.info("[OK] UC-1.3 callbacks registered successfully")

        # UC-1.4: Proportional Functional Diversity of Samples
        logger.debug("Registering UC-1.4 callbacks...")
        register_uc_1_4_callbacks(app, plot_service)
        logger.info("[OK] UC-1.4 callbacks registered successfully")

        # UC-1.5: Regulatory Compliance Scorecard
        logger.debug("Registering UC-1.5 callbacks...")
        register_uc_1_5_callbacks(app, plot_service)
        logger.info("[OK] UC-1.5 callbacks registered successfully")

        # UC-1.6: Sample-Agency Functional Potential Heatmap
        logger.debug("Registering UC-1.6 callbacks...")
        register_uc_1_6_callbacks(app, plot_service)
        logger.info("[OK] UC-1.6 callbacks registered successfully")

        logger.info("Module 1 callback registration complete")

    except Exception as e:
        logger.error(f"Failed to register Module 1 callbacks: {e}", exc_info=True)
        raise

register_module2_callbacks

register_module2_callbacks(app, plot_service) -> None

Register all Module 2 callbacks with Dash app.

Parameters:

Name Type Description Default
app Dash

Dash application instance.

required
plot_service PlotService

Singleton PlotService instance (shared across all callbacks).

required
Notes
  • Registers callback handlers for Module 2 use cases
  • Refer to official documentation for supported use case details
Source code in src/presentation/callbacks/module_callbacks/module2_callbacks.py
def register_module2_callbacks(app, plot_service) -> None:
    """
    Register all Module 2 callbacks with Dash app.

    Parameters
    ----------
    app : Dash
        Dash application instance.
    plot_service : PlotService
        Singleton PlotService instance (shared across all callbacks).

    Notes
    -----
    - Registers callback handlers for Module 2 use cases
    - Refer to official documentation for supported use case details
    """
    logger.info("=" * 60)
    logger.info("REGISTERING MODULE 2 CALLBACKS...")
    logger.info("=" * 60)

    # Register UC-2.1: Ranking of Samples by KO Richness
    logger.info("→ Registering UC-2.1...")
    register_uc_2_1_callbacks(app, plot_service)
    logger.info("[OK] UC-2.1 callbacks registered (KO richness analysis)")

    # Register UC-2.2: Ranking of Samples by Compound Richness
    logger.info("→ Registering UC-2.2...")
    register_uc_2_2_callbacks(app, plot_service)
    logger.info("[OK] UC-2.2 callbacks registered (Compound richness analysis)")

    # Register UC-2.3: Ranking of Compounds by Sample Diversity per Class
    logger.info("→ Registering UC-2.3...")
    register_uc_2_3_callbacks(app, plot_service)
    logger.info("[OK] UC-2.3 callbacks registered (Compound-sample diversity)")

    # Register UC-2.4: Ranking of Compounds by Genetic Interaction per Class
    logger.info("→ Registering UC-2.4...")
    register_uc_2_4_callbacks(app, plot_service)
    logger.info("[OK] UC-2.4 callbacks registered (Compound-gene diversity)")

    # Register UC-2.5: Distribution of KO Across Samples
    logger.info("→ Registering UC-2.5...")
    register_uc_2_5_callbacks(app, plot_service)
    logger.info("[OK] UC-2.5 callbacks registered (KO distribution)")

    logger.info("=" * 60)
    logger.info("[OK] ALL MODULE 2 CALLBACKS REGISTERED SUCCESSFULLY")
    logger.info("=" * 60)

register_module3_callbacks

register_module3_callbacks(app, plot_service) -> None

Register all Module 3 callbacks with Dash app.

Parameters:

Name Type Description Default
app Dash

Dash application instance.

required
plot_service PlotService

Singleton PlotService instance (shared across all callbacks).

required
Notes
  • Registers callback handlers for Module 3 use cases
  • Refer to official documentation for supported use case details
Source code in src/presentation/callbacks/module_callbacks/module3_callbacks.py
def register_module3_callbacks(app, plot_service) -> None:
    """
    Register all Module 3 callbacks with Dash app.

    Parameters
    ----------
    app : Dash
        Dash application instance.
    plot_service : PlotService
        Singleton PlotService instance (shared across all callbacks).

    Notes
    -----
    - Registers callback handlers for Module 3 use cases
    - Refer to official documentation for supported use case details
    """
    logger.info("=" * 60)
    logger.info("REGISTERING MODULE 3 CALLBACKS...")
    logger.info("=" * 60)

    # Register UC-3.1: PCA - Sample Relationships by KO Profile
    logger.info("→ Registering UC-3.1...")
    register_uc_3_1_callbacks(app, plot_service)
    logger.info("[OK] UC-3.1 callbacks registered (PCA - KO profile)")

    # Register UC-3.2: PCA - Sample Relationships by Chemical Profile
    logger.info("→ Registering UC-3.2...")
    register_uc_3_2_callbacks(app, plot_service)
    logger.info("[OK] UC-3.2 callbacks registered (PCA - Compound profile)")

    # Register UC-3.3: Interactive Hierarchical Clustering of Samples
    logger.info("→ Registering UC-3.3...")
    register_uc_3_3_callbacks(app, plot_service)
    logger.info("[OK] UC-3.3 callbacks registered (Hierarchical clustering)")

    # Register UC-3.4: Sample Similarity Based on KO Profiles
    logger.info("→ Registering UC-3.4...")
    register_uc_3_4_callbacks(app, plot_service)
    logger.info("[OK] UC-3.4 callbacks registered (Sample similarity - KO)")

    # Register UC-3.5: Sample Similarity Based on Compound Profiles
    logger.info("→ Registering UC-3.5...")
    register_uc_3_5_callbacks(app, plot_service)
    logger.info("[OK] UC-3.5 callbacks registered (Sample similarity - Compound)")

    # Register UC-3.6: Gene Co-occurrence Patterns Across Samples
    logger.info("→ Registering UC-3.6...")
    register_uc_3_6_callbacks(app, plot_service)
    logger.info("[OK] UC-3.6 callbacks registered (Gene symbol co-occurrence)")

    # Register UC-3.7: Compound Co-occurrence Patterns Across Samples
    logger.info("→ Registering UC-3.7...")
    register_uc_3_7_callbacks(app, plot_service)
    logger.info("[OK] UC-3.7 callbacks registered (Compound co-occurrence)")

    logger.info("=" * 60)
    logger.info("[OK] ALL MODULE 3 CALLBACKS REGISTERED SUCCESSFULLY")
    logger.info("=" * 60)

register_module4_callbacks

register_module4_callbacks(app, plot_service) -> None

Register all Module 4 callbacks with Dash app.

Parameters:

Name Type Description Default
app Dash

Dash application instance.

required
plot_service PlotService

Singleton PlotService instance (shared across all callbacks).

required
Notes
  • Registers callback handlers for Module 4 use cases
  • Refer to official documentation for supported use case details
Source code in src/presentation/callbacks/module_callbacks/module4_callbacks.py
def register_module4_callbacks(app, plot_service) -> None:
    """
    Register all Module 4 callbacks with Dash app.

    Parameters
    ----------
    app : Dash
        Dash application instance.
    plot_service : PlotService
        Singleton PlotService instance (shared across all callbacks).

    Notes
    -----
    - Registers callback handlers for Module 4 use cases
    - Refer to official documentation for supported use case details
    """
    logger.info("=" * 60)
    logger.info("REGISTERING MODULE 4 CALLBACKS...")
    logger.info("=" * 60)

    # Register UC-4.1: Interactive Functional Profiling by Pathway
    logger.info("→ Registering UC-4.1...")
    register_uc_4_1_callbacks(app, plot_service)
    logger.info("[OK] UC-4.1 callbacks registered (Pathway functional profiling)")

    # Register UC-4.2: Interactive Sample Ranking by Pathway
    logger.info("→ Registering UC-4.2...")
    register_uc_4_2_callbacks(app, plot_service)
    logger.info("[OK] UC-4.2 callbacks registered (Sample ranking by pathway richness)")

    # Register UC-4.3: Interactive Sample Comparison by Pathway (Radar)
    logger.info("→ Registering UC-4.3...")
    register_uc_4_3_callbacks(app, plot_service)
    logger.info(
        "[OK] UC-4.3 callbacks registered (Sample comparison by pathway - Radar)"
    )

    # Register UC-4.4: Interactive Functional Fingerprint by Sample (Radar)
    logger.info("→ Registering UC-4.4...")
    register_uc_4_4_callbacks(app, plot_service)
    logger.info(
        "[OK] UC-4.4 callbacks registered (Functional fingerprint by sample - Radar)"
    )

    # Register UC-4.5: Interactive Gene Presence Map by Pathway
    logger.info("→ Registering UC-4.5...")
    register_uc_4_5_callbacks(app, plot_service)
    logger.info("[OK] UC-4.5 callbacks registered (Gene presence map by pathway)")

    # Register UC-4.6: Interactive Functional Potential by Compound
    logger.info("→ Registering UC-4.6...")
    register_uc_4_6_callbacks(app, plot_service)
    logger.info(
        "[OK] UC-4.6 callbacks registered " "(Functional potential by compound)"
    )

    # Register UC-4.9: Interactive Enzymatic Activity Profiling
    logger.info("→ Registering UC-4.9...")
    register_uc_4_9_callbacks(app, plot_service)
    logger.info("[OK] UC-4.9 callbacks registered " "(Enzymatic activity profiling)")

    # Register UC-4.7: Interactive Gene-Compound Association Explorer
    logger.info("→ Registering UC-4.7...")
    register_uc_4_7_callbacks(app, plot_service)
    logger.info(
        "[OK] UC-4.7 callbacks registered " "(Gene-compound association explorer)"
    )

    # Register UC-4.8: Interactive Gene Inventory Explorer
    logger.info("→ Registering UC-4.8...")
    register_uc_4_8_callbacks(app, plot_service)
    logger.info(
        "[OK] UC-4.8 callbacks registered " "(Gene inventory explorer by sample)"
    )

    # Register UC-4.10: Genetic Diversity of Enzymatic Activities
    logger.info("→ Registering UC-4.10...")
    register_uc_4_10_callbacks(app, plot_service)
    logger.info(
        "[OK] UC-4.10 callbacks registered " "(Genetic diversity by enzyme activity)"
    )

    # Register UC-4.11: Global Hierarchical View of Genetic Diversity (HADEG)
    logger.info("→ Registering UC-4.11...")
    register_uc_4_11_callbacks(app, plot_service)
    logger.info(
        "[OK] UC-4.11 callbacks registered "
        "(Global genetic diversity hierarchy - HADEG Sunburst)"
    )

    # Register UC-4.12: Interactive Pathway Relationships by Sample (HADEG)
    logger.info("→ Registering UC-4.12...")
    register_uc_4_12_callbacks(app, plot_service)
    logger.info(
        "[OK] UC-4.12 callbacks registered " "(Pathway relationships by sample - HADEG)"
    )

    # Register UC-4.13: Interactive Genetic Profile by Compound Pathway (HADEG)
    logger.info("→ Registering UC-4.13...")
    register_uc_4_13_callbacks(app, plot_service)
    logger.info(
        "[OK] UC-4.13 callbacks registered "
        "(Genetic profile by compound pathway - HADEG)"
    )

    logger.info("=" * 60)
    logger.info("[OK] ALL MODULE 4 CALLBACKS REGISTERED SUCCESSFULLY")
    logger.info("=" * 60)

register_module5_callbacks

register_module5_callbacks(app, plot_service) -> None

Register all Module 5 callbacks with Dash app.

Parameters:

Name Type Description Default
app Dash

Dash application instance.

required
plot_service PlotService

Singleton PlotService instance (shared across all callbacks).

required
Notes
  • Registers callback handlers for Module 5 use cases
  • Refer to official documentation for supported use case details
Source code in src/presentation/callbacks/module_callbacks/module5_callbacks.py
def register_module5_callbacks(app, plot_service) -> None:
    """
    Register all Module 5 callbacks with Dash app.

    Parameters
    ----------
    app : Dash
        Dash application instance.
    plot_service : PlotService
        Singleton PlotService instance (shared across all callbacks).

    Notes
    -----
    - Registers callback handlers for Module 5 use cases
    - Refer to official documentation for supported use case details
    """
    logger.info("=" * 60)
    logger.info("REGISTERING MODULE 5 CALLBACKS...")
    logger.info("=" * 60)

    # Register UC-5.1: Sample - Compound Class Interaction Strength
    logger.info("-> Registering UC-5.1...")
    register_uc_5_1_callbacks(app, plot_service)
    logger.info("UC-5.1 callbacks registered (Chord Diagram)")

    # Register UC-5.2: Sample Similarity Based on Shared Chemical Profiles
    logger.info("-> Registering UC-5.2...")
    register_uc_5_2_callbacks(app, plot_service)
    logger.info("UC-5.2 callbacks registered (Chord Diagram - Pairwise Mode)")

    # Register UC-5.3: Regulatory Relevance of Samples
    logger.info("-> Registering UC-5.3...")
    register_uc_5_3_callbacks(app, plot_service)
    logger.info("UC-5.3 callbacks registered (Chord Diagram - Dropdown Trigger)")

    # Register UC-5.4: Gene-Compound Interaction Network
    logger.info("-> Registering UC-5.4...")
    register_uc_5_4_callbacks(app, plot_service)
    logger.info("UC-5.4 callbacks registered (Network Diagram - Accordion Trigger)")

    # Register UC-5.5: Gene-Gene Functional Interaction Network
    logger.info("-> Registering UC-5.5...")
    register_uc_5_5_callbacks(app, plot_service)
    logger.info("UC-5.5 callbacks registered (Similarity Network - Accordion Trigger)")

    # Register UC-5.6: Compound-Compound Functional Similarity Network
    logger.info("-> Registering UC-5.6...")
    register_uc_5_6_callbacks(app, plot_service)
    logger.info("UC-5.6 callbacks registered (Similarity Network - Accordion Trigger)")

    logger.info("=" * 60)
    logger.info("ALL MODULE 5 CALLBACKS REGISTERED SUCCESSFULLY")
    logger.info("=" * 60)

register_module6_callbacks

register_module6_callbacks(app, plot_service) -> None

Register all Module 6 callbacks with Dash app.

Parameters:

Name Type Description Default
app Dash

Dash application instance.

required
plot_service PlotService

Singleton PlotService instance (shared across all callbacks).

required
Notes
  • Registers callback handlers for Module 6 use cases
  • Refer to official documentation for supported use case details
Source code in src/presentation/callbacks/module_callbacks/module6_callbacks.py
def register_module6_callbacks(app, plot_service) -> None:
    """
    Register all Module 6 callbacks with Dash app.

    Parameters
    ----------
    app : Dash
        Dash application instance.
    plot_service : PlotService
        Singleton PlotService instance (shared across all callbacks).

    Notes
    -----
    - Registers callback handlers for Module 6 use cases
    - Refer to official documentation for supported use case details
    """
    logger.info("=" * 60)
    logger.info("REGISTERING MODULE 6 CALLBACKS...")
    logger.info("=" * 60)

    # Register UC-6.1: Regulatory to Molecular Interaction Flow
    logger.info("→ Registering UC-6.1...")
    register_uc_6_1_callbacks(app, plot_service)
    logger.info("[OK] UC-6.1 callbacks registered (Regulatory-Molecular Sankey)")

    # Register UC-6.2: Biological Interaction Flow
    logger.info("→ Registering UC-6.2...")
    register_uc_6_2_callbacks(app, plot_service)
    logger.info("[OK] UC-6.2 callbacks registered (Biological Interaction Sankey)")

    # Register UC-6.3: Chemical Hierarchy of Bioremediation
    logger.info("→ Registering UC-6.3...")
    register_uc_6_3_callbacks(app, plot_service)
    logger.info("[OK] UC-6.3 callbacks registered (Chemical Hierarchy Treemap)")

    # Register UC-6.4: Overview of Enzymatic Activity and Substrate Scope
    logger.info("→ Registering UC-6.4...")
    register_uc_6_4_callbacks(app, plot_service)
    logger.info("[OK] UC-6.4 callbacks registered (Enzymatic Activity Treemap)")

    # Register UC-6.5: Chemical-Enzymatic Landscape by Substrate Scope
    logger.info("→ Registering UC-6.5...")
    register_uc_6_5_callbacks(app, plot_service)
    logger.info("[OK] UC-6.5 callbacks registered (Chemo-Enzymatic Landscape)")

    logger.info("=" * 60)
    logger.info("[OK] ALL MODULE 6 CALLBACKS REGISTERED SUCCESSFULLY")
    logger.info("=" * 60)

register_module7_callbacks

register_module7_callbacks(app, plot_service) -> None

Register all Module 7 callbacks with Dash app.

Parameters:

Name Type Description Default
app Dash

Dash application instance.

required
plot_service PlotService

Singleton PlotService instance (shared across all callbacks).

required
Notes
  • Registers callback handlers for Module 7 use cases
  • Refer to official documentation for supported use case details
Source code in src/presentation/callbacks/module_callbacks/module7_callbacks.py
def register_module7_callbacks(app, plot_service) -> None:
    """
    Register all Module 7 callbacks with Dash app.

    Parameters
    ----------
    app : Dash
        Dash application instance.
    plot_service : PlotService
        Singleton PlotService instance (shared across all callbacks).

    Notes
    -----
    - Registers callback handlers for Module 7 use cases
    - Refer to official documentation for supported use case details
    """
    logger.info("=" * 60)
    logger.info("REGISTERING MODULE 7 CALLBACKS...")
    logger.info("=" * 60)

    # Register UC-7.1: Faceted Heatmap of Predicted Compound Toxicity Profiles
    logger.info("→ Registering UC-7.1...")
    register_uc_7_1_callbacks(app, plot_service)
    logger.info("[OK] UC-7.1 callbacks registered (Toxicity fingerprints)")

    # Register UC-7.2: Concordance Between Predicted Risk and Regulatory Focus
    logger.info("→ Registering UC-7.2...")
    register_uc_7_2_callbacks(app, plot_service)
    logger.info("[OK] UC-7.2 callbacks registered (Risk-Regulatory Concordance)")

    # Register UC-7.3: Elite Specialist Identification
    logger.info("→ Registering UC-7.3...")
    register_uc_7_3_callbacks(app, plot_service)
    logger.info("[OK] UC-7.3 callbacks registered (Genetic Response Mapping)")

    # Register UC-7.4: Toxicity Score Distribution
    logger.info("→ Registering UC-7.4...")
    register_uc_7_4_callbacks(app, plot_service)
    logger.info("[OK] UC-7.4 callbacks registered (Toxicity endpoint distribution)")

    # Register UC-7.5: Interactive Distribution of Toxicity Scores by Endpoint Category
    logger.info("→ Registering UC-7.5...")
    register_uc_7_5_callbacks(app, plot_service)
    logger.info("[OK] UC-7.5 callbacks registered (Density plot toxicity distribution)")

    # Register UC-7.6: Sample Risk Mitigation Breadth by Compound Variety
    logger.info("→ Registering UC-7.6...")
    register_uc_7_6_callbacks(app, plot_service)
    logger.info("[OK] UC-7.6 callbacks registered (Risk Mitigation Breadth)")

    # Register UC-7.7: Sample Risk Mitigation Depth Profile by Genetic Investment
    logger.info("→ Registering UC-7.7...")
    register_uc_7_7_callbacks(app, plot_service)
    logger.info("[OK] UC-7.7 callbacks registered (Risk Mitigation Depth)")

    logger.info("=" * 60)
    logger.info("[OK] ALL MODULE 7 CALLBACKS REGISTERED SUCCESSFULLY")
    logger.info("=" * 60)

register_module8_callbacks

register_module8_callbacks(app, plot_service) -> None

Register all Module 8 callbacks with Dash app.

Parameters:

Name Type Description Default
app Dash

Dash application instance.

required
plot_service PlotService

Singleton PlotService instance (shared across all callbacks).

required
Notes
  • Registers callback handlers for Module 8 use cases
  • Refer to official documentation for supported use case details
Source code in src/presentation/callbacks/module_callbacks/module8_callbacks.py
def register_module8_callbacks(app, plot_service) -> None:
    """
    Register all Module 8 callbacks with Dash app.

    Parameters
    ----------
    app : dash.Dash
        Dash application instance.
    plot_service : PlotService
        Singleton PlotService instance (shared across all callbacks).

    Notes
    -----
    - Registers callback handlers for Module 8 use cases
    - Refer to official documentation for supported use case details
    """
    logger.info("=" * 60)
    logger.info("REGISTERING MODULE 8 CALLBACKS")
    logger.info("=" * 60)

    # UC-8.1: Minimal Sample Grouping for Complete Compound Coverage
    logger.info("[Module 8] Registering UC-8.1 callbacks...")
    register_uc_8_1_callbacks(app, plot_service)
    logger.info("[Module 8] [OK] UC-8.1 callbacks registered successfully")

    # UC-8.2: Chemical Class Completeness Scorecard
    logger.info("[Module 8] Registering UC-8.2 callbacks...")
    register_uc_8_2_callbacks(app, plot_service)
    logger.info("[Module 8] [OK] UC-8.2 callbacks registered successfully")

    # UC-8.3: Compound-Specific KO Completeness Scorecard
    logger.info("[Module 8] Registering UC-8.3 callbacks...")
    register_uc_8_3_callbacks(app, plot_service)
    logger.info("[Module 8] [OK] UC-8.3 callbacks registered successfully")

    # UC-8.4: Pathway Completeness Scorecard for HADEG Pathways
    logger.info("[Module 8] Registering UC-8.4 callbacks...")
    register_uc_8_4_callbacks(app, plot_service)
    logger.info("[Module 8] \u2713 UC-8.4 callbacks registered successfully")

    # UC-8.5: KEGG Pathway Completeness Scorecard
    logger.info("[Module 8] Registering UC-8.5 callbacks...")
    register_uc_8_5_callbacks(app, plot_service)
    logger.info("[Module 8] \u2713 UC-8.5 callbacks registered successfully")

    # UC-8.6: Pathway-Centric Consortium Design by KO Coverage
    logger.info("[Module 8] Registering UC-8.6 callbacks...")
    register_uc_8_6_callbacks(app, plot_service)
    logger.info("[Module 8] [OK] UC-8.6 callbacks registered successfully")

    # UC-8.7: Intersection of Genes Across Samples
    logger.info("[Module 8] Registering UC-8.7 callbacks...")
    register_uc_8_7_callbacks(app, plot_service)
    logger.info("[Module 8] [OK] UC-8.7 callbacks registered successfully")

    logger.info("=" * 60)
    logger.info("MODULE 8 CALLBACKS REGISTRATION COMPLETE")
    logger.info("=" * 60)

register_processing_callbacks

register_processing_callbacks(app, data_processor: Optional[DataProcessor] = None, progress_tracker: Optional[ProgressTracker] = None)

Register processing callbacks.

Parameters:

Name Type Description Default
app Dash

Dash application instance

required
data_processor Optional[DataProcessor]

DataProcessor instance, by default None

None
progress_tracker Optional[ProgressTracker]

ProgressTracker instance, by default None

None
Notes

DESATIVADO - Usando real_processing_callbacks.py

Este arquivo contém callbacks da Application Layer que foram substituídos pelos callbacks reais.

Source code in src/presentation/callbacks/processing_callbacks.py
def register_processing_callbacks(
    app,
    data_processor: Optional[DataProcessor] = None,
    progress_tracker: Optional[ProgressTracker] = None,
):
    """
    Register processing callbacks.

    Parameters
    ----------
    app : Dash
        Dash application instance
    data_processor : Optional[DataProcessor], optional
        DataProcessor instance, by default None
    progress_tracker : Optional[ProgressTracker], optional
        ProgressTracker instance, by default None

    Notes
    -----
    DESATIVADO - Usando real_processing_callbacks.py

    Este arquivo contém callbacks da Application Layer que foram
    substituídos pelos callbacks reais.
    """
    # DESATIVADO - Callbacks duplicados, usando real_processing_callbacks.py
    return

    # Initialize services if not provided
    if data_processor is None:
        data_processor = DataProcessor()
    if progress_tracker is None:
        progress_tracker = ProgressTracker()

    # CALLBACK DESATIVADO - Conflita com real_processing_callbacks.py
    # @callback(
    #     [
    #         Output("progress-panel", "style"),
    #         Output("progress-interval", "disabled"),
    #         Output("session-id-store", "data")
    #     ],
    #     [Input("process-data-btn", "n_clicks")],
    #     [
    #         State("upload-result-store", "data"),
    #         State("session-id-store", "data")
    #     ],
    #     prevent_initial_call=True
    # )
    def start_processing_disabled(
        n_clicks: Optional[int],
        upload_result: Optional[Dict[str, Any]],
        session_id: Optional[str],
    ) -> Tuple[Dict, bool, str]:
        """
        Start data processing workflow.

        Parameters
        ----------
        n_clicks : Optional[int]
            Button click count
        upload_result : Optional[Dict[str, Any]]
            UploadResultDTO from store
        session_id : Optional[str]
            Current session ID

        Returns
        -------
        Tuple[Dict, bool, str]
            - Progress panel style (display: block)
            - Interval disabled flag (False to enable)
            - Session ID (new or existing)

        Notes
        -----
        - Generates session ID if not exists
        - Starts background processing task
        - Enables progress interval for updates
        - Shows progress panel
        """
        if not n_clicks or not upload_result:
            raise PreventUpdate

        # Generate session ID if needed
        if not session_id:
            session_id = str(uuid.uuid4())

        try:
            # TODO: Start async processing task
            # For now, we'll track progress synchronously
            # In production, use Celery or similar for background tasks

            # Initialize progress
            progress_tracker.start_processing(session_id, total_stages=8)

            # Show progress panel and enable interval
            return {"display": "block"}, False, session_id

        except Exception as e:
            # Handle errors
            return {"display": "none"}, True, session_id

    # CALLBACK DESATIVADO - Conflita com real_processing_callbacks.py
    # @callback(
    #     [
    #         Output("processing-progress-store", "data"),
    #         Output("progress-display", "children")
    #     ],
    #     [Input("progress-interval", "n_intervals")],
    #     [State("session-id-store", "data")],
    #     prevent_initial_call=True
    # )
    def update_progress_disabled(
        n_intervals: int, session_id: Optional[str]
    ) -> Tuple[Optional[Dict[str, Any]], Any]:
        """
        Update processing progress.

        Parameters
        ----------
        n_intervals : int
            Number of interval ticks
        session_id : Optional[str]
            Current session ID

        Returns
        -------
        Tuple[Optional[Dict[str, Any]], Any]
            - ProcessingProgressDTO as dict
            - Updated progress bar component

        Notes
        -----
        - Polls ProgressTracker every 1 second
        - Updates progress bar display
        - Stores progress data for completion check
        """
        if not session_id:
            raise PreventUpdate

        try:
            # Get current progress
            progress = progress_tracker.get_progress(session_id)

            if not progress:
                raise PreventUpdate

            # Create progress bar component
            from ..components.base import create_progress_bar

            progress_bar = create_progress_bar(progress_data=progress.to_dict())

            return progress.to_dict(), progress_bar.children

        except Exception:
            raise PreventUpdate

    # Callback desativado - usando real_processing_callbacks.py
    # @callback(
    #     [
    #         Output("completion-panel", "style"),
    #         Output("completion-panel", "children"),
    #         Output("progress-interval", "disabled", allow_duplicate=True),
    #         Output("processing-complete-store", "data")
    #     ],
    #     [Input("processing-progress-store", "data")],
    #     prevent_initial_call=True
    # )
    def show_completion_disabled(
        progress_data: Optional[Dict[str, Any]]
    ) -> Tuple[Dict[str, str], Any, bool, bool]:
        """
        Show completion panel when processing finishes.

        Parameters
        ----------
        progress_data : Optional[Dict[str, Any]]
            ProcessingProgressDTO data

        Returns
        -------
        Tuple[Dict, Any, bool, bool]
            - Completion panel style
            - Completion panel component
            - Interval disabled (True to stop polling)
            - Processing complete flag

        Notes
        -----
        - Checks if percentage == 100
        - Disables progress interval
        - Shows completion panel
        - Sets complete flag to True
        """
        if not progress_data:
            raise PreventUpdate

        percentage = progress_data.get("percentage", 0)

        # Check if complete
        if percentage >= 100:
            # Create completion data
            completion_data = {
                "success": True,
                "processing_time": 45.2,  # TODO: Get actual time
                "total_samples": 150,  # TODO: Get from result
                "merged_records": 1200,  # TODO: Get from result
                "session_id": "abc123",  # TODO: Get actual session_id
            }

            # Create completion panel
            from ..components.composite import create_completion_panel

            panel = create_completion_panel(completion_data=completion_data)

            return {"display": "block"}, panel.children, True, True

        raise PreventUpdate

    # CALLBACK DESATIVADO - Conflita com real_processing_callbacks.py
    # @callback(
    #     Output("url", "pathname"),
    #     [Input("view-results-btn", "n_clicks")],
    #     prevent_initial_call=True
    # )
    def navigate_to_results_disabled(n_clicks: Optional[int]) -> str:
        """
        Navigate to results page.

        Parameters
        ----------
        n_clicks : Optional[int]
            Button click count

        Returns
        -------
        str
            URL pathname (/results)

        Notes
        -----
        - Triggered by "View Results" button
        - Changes URL to /results
        - Results page will load merged data from store
        """
        if not n_clicks:
            raise PreventUpdate

        return "/results"

register_real_processing_callbacks

register_real_processing_callbacks(app)

Register real processing callbacks with progress tracking.

Parameters:

Name Type Description Default
app Dash

Dash application instance

required
Source code in src/presentation/callbacks/real_processing_callbacks.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
def register_real_processing_callbacks(app):
    """
    Register real processing callbacks with progress tracking.

    Parameters
    ----------
    app : Dash
        Dash application instance
    """
    logger.info("=" * 60)
    logger.info("Registering REAL PROCESSING callbacks with long_callback...")
    logger.info("=" * 60)
    use_background_callbacks = bool(
        app.server.config.get("BIOREMPP_BACKGROUND_CALLBACKS_ENABLED", True)
    )
    execution_mode = "background" if use_background_callbacks else "synchronous"
    logger.info(f"Processing callback execution mode: {execution_mode}")

    # Background callback for data processing with simple spinner
    @app.callback(
        output=[
            Output("processing-status", "children"),
            Output("merged-result-store", "data"),
            Output("results-context-store", "data"),
            Output("completion-panel", "style"),
            Output("processing-progress", "style"),
            Output("resume-browser-token-store", "data", allow_duplicate=True),
        ],
        inputs=Input("process-data-btn", "n_clicks"),
        state=[
            State("upload-data-store", "data"),
            State("example-data-store", "data"),
            State("resume-browser-token-store", "data"),
        ],
        running=[
            (
                Output("process-data-btn", "disabled"),
                True,  # Disable during processing
                False,  # Enable when complete
            )
        ],
        background=use_background_callbacks,
        prevent_initial_call=True,
    )
    @instrument_callback("processing.process_data_with_spinner")
    def process_data_with_spinner(n_clicks, upload_data, example_data, owner_token):
        """
        Process uploaded or example data with real-time progress tracking.

        Parameters
        ----------
        set_progress : callable
            Function to update progress UI
        n_clicks : int
            Process button clicks
        upload_data : dict
            Uploaded file data
        example_data : dict
            Example file data
        owner_token : str
            Browser ownership token used for resume-by-job-id binding

        Returns
        -------
        tuple
            (
                status_message,
                merged_data,
                results_context,
                completion_panel_style,
                progress_panel_style,
                owner_token_store_update,
            )
        """
        if n_clicks is None:
            raise PreventUpdate
        callback_started_at = time.perf_counter()

        # Determine data source
        file_data = upload_data or example_data

        if not file_data:
            _observe_processing_duration(callback_started_at, "no_data")
            return (
                dbc.Alert(
                    [
                        html.I(className="fas fa-exclamation-circle me-2"),
                        "No data to process. Please upload a file or "
                        "load example data first.",
                    ],
                    color="warning",
                ),
                no_update,
                no_update,
                {"display": "none"},  # Keep progress hidden
                no_update,
                no_update,
            )

        processing_outcome = "unknown"
        try:
            service = get_data_service()
            job_id = DataProcessingService.generate_job_id()
            effective_owner_token = owner_token or str(uuid4())
            generated_owner_token = not bool(owner_token)

            logger.info(
                "Starting data processing",
                extra={
                    "job_ref": _job_ref(job_id),
                    "file_name": file_data.get("filename", "unknown"),
                    "sample_count": file_data.get("sample_count"),
                    "ko_count": file_data.get("ko_count"),
                },
            )
            if generated_owner_token:
                logger.info(
                    "Resume owner token was missing and has been generated for this run",
                    extra={"job_ref": _job_ref(job_id)},
                )

            # Process data (all merges happen here)
            result = service.process_upload(
                content=file_data["content"],
                filename=file_data["filename"],
                job_id=job_id,
            )

            logger.info(
                "Processing completed successfully",
                extra={
                    "metadata": result.get("metadata", {}),
                    "biorempp_rows": result["biorempp_df"].shape[0],
                    "kegg_rows": result["kegg_df"].shape[0],
                    "hadeg_rows": result["hadeg_df"].shape[0],
                    "toxcsm_rows": result["toxcsm_df"].shape[0],
                },
            )

            # Log DataFrame structures for debugging
            logger.debug(
                "DataFrame structures",
                extra={
                    "biorempp": {
                        "shape": result["biorempp_df"].shape,
                        "columns": result["biorempp_df"].columns.tolist(),
                    },
                    "hadeg": {
                        "shape": result["hadeg_df"].shape,
                        "columns": result["hadeg_df"].columns.tolist(),
                    },
                    "toxcsm": {
                        "shape": result["toxcsm_df"].shape,
                        "columns": result["toxcsm_df"].columns.tolist(),
                    },
                    "kegg": {
                        "shape": result["kegg_df"].shape,
                        "columns": result["kegg_df"].columns.tolist(),
                    },
                },
            )

            # Debug logging for ToxCSM fields before serialization
            logger.info(f"[DEBUG] ToxCSM fields before serialization:")
            logger.info(f"  - toxcsm_raw_df in result: {'toxcsm_raw_df' in result}")
            if "toxcsm_raw_df" in result:
                toxcsm_raw = result["toxcsm_raw_df"]
                logger.info(f"  - toxcsm_raw_df type: {type(toxcsm_raw)}")
                logger.info(
                    f"  - toxcsm_raw_df shape: {toxcsm_raw.shape if hasattr(toxcsm_raw, 'shape') else 'N/A'}"
                )

            # Convert DataFrames to dict for JSON serialization
            serialized_result = {
                "biorempp_df": result["biorempp_df"].to_dict("records"),
                "biorempp_raw_df": result["biorempp_raw_df"].to_dict("records"),
                "hadeg_df": result["hadeg_df"].to_dict("records"),
                "hadeg_raw_df": result["hadeg_raw_df"].to_dict("records"),
                "toxcsm_df": result["toxcsm_df"].to_dict(
                    "records"
                ),  # Processed (5 cols for graphs)
                "toxcsm_raw_df": result["toxcsm_raw_df"].to_dict(
                    "records"
                ),  # Merged (66 cols for table & download)
                "kegg_df": result["kegg_df"].to_dict("records"),
                "kegg_raw_df": result["kegg_raw_df"].to_dict("records"),
                "metadata": result["metadata"],
            }

            logger.info(f"[DEBUG] After serialization:")
            logger.info(
                f"  - toxcsm_raw_df records: {len(serialized_result.get('toxcsm_raw_df', []))}"
            )

            logger.info("DataFrames serialized successfully")

            # Persist serialized payload for resume-by-job-id flow (non-blocking)
            metadata = result["metadata"]
            persisted_job_id = metadata.get("job_id", job_id)
            resume_saved = _persist_resume_payload_with_timeout(
                job_id=persisted_job_id,
                payload=serialized_result,
                owner_token=effective_owner_token,
                ttl_seconds=job_resume_service.get_resume_ttl_seconds(),
            )
            payload_size_bytes = job_resume_service.estimate_payload_size_bytes(
                serialized_result
            )

            logger.info(
                "Resume payload persistence result",
                extra={
                    "job_ref": _job_ref(persisted_job_id),
                    "resume_saved": resume_saved,
                    "payload_size_bytes": payload_size_bytes,
                    "resume_max_payload_mb": job_resume_service.get_resume_max_payload_mb(),
                },
            )

            if not resume_saved:
                logger.warning(
                    "Resume payload unavailable for this run",
                    extra={"job_ref": _job_ref(persisted_job_id)},
                )

            status_message = create_processing_alert(
                "success",
                "Processing completed successfully!",
                details={"Job ID": persisted_job_id},
                notes=[
                    "This identifier lets you resume analysis without reprocessing.",
                    "On the results page, you can copy it for later use.",
                ],
            )

            # Return results
            processing_outcome = "success"
            return (
                status_message,
                serialized_result,
                build_results_context(serialized_result),
                {"display": "block"},  # Show completion panel
                {"display": "none"},  # Hide progress panel
                effective_owner_token,
            )

        # Validation errors (expected)
        except ValidationError as e:
            processing_outcome = "validation_error"
            logger.warning(
                f"Data validation error: {str(e)}",
                extra={
                    "file_name": file_data.get("filename", "unknown"),
                    "error_type": "ValidationError",
                },
            )

            return (
                create_processing_error_alert(
                    "Data Validation Error",
                    "Unable to process data due to validation issues.",
                    error_type="ValidationError",
                    recovery_suggestions=[
                        "Check file format and structure",
                        "Ensure KO IDs follow the correct pattern (K + 5 digits)",
                        "Verify sample names are valid",
                        "Try the example dataset to confirm app is working",
                    ],
                    technical_details=str(e),
                ),
                no_update,
                no_update,
                {"display": "none"},
                no_update,
                no_update,
            )

        # Timeout errors
        except DataProcessingTimeoutError as e:
            processing_outcome = "timeout"
            logger.error(
                f"Processing timeout: {str(e)}",
                extra={"file_name": file_data.get("filename", "unknown")},
            )

            return (
                create_processing_error_alert(
                    "Processing Timeout",
                    "Processing took too long and was stopped.",
                    error_type="TimeoutError",
                    recovery_suggestions=[
                        "The file may be too large or complex",
                        "Try reducing the number of samples",
                        "Retry the processing",
                        "Contact support if problem persists",
                    ],
                    technical_details=str(e),
                ),
                no_update,
                no_update,
                {"display": "none"},
                no_update,
                no_update,
            )

        # Empty DataFrame errors
        except EmptyDataFrameError as e:
            processing_outcome = "empty_dataframe"
            logger.error(
                f"Empty DataFrame error: {str(e)}",
                extra={"file_name": file_data.get("filename", "unknown")},
            )

            return (
                create_processing_error_alert(
                    "No Data Matched",
                    "No data was found in the database for your input.",
                    error_type="EmptyDataError",
                    recovery_suggestions=[
                        "Check that your KO IDs exist in the database",
                        "Verify the KO ID format is correct",
                        "Try with different samples",
                        "Use the example dataset to verify functionality",
                    ],
                    technical_details=str(e),
                ),
                no_update,
                no_update,
                {"display": "none"},
                no_update,
                no_update,
            )

        # Circuit breaker errors
        except CircuitBreakerOpenError as e:
            processing_outcome = "circuit_breaker"
            logger.error(
                f"Circuit breaker open: {str(e)}",
                extra={"file_name": file_data.get("filename", "unknown")},
            )

            return (
                create_processing_error_alert(
                    "Service Temporarily Unavailable",
                    "A database service is currently unavailable.",
                    error_type="CircuitBreakerError",
                    recovery_suggestions=[
                        "Wait a moment and try again",
                        "The service should recover automatically",
                        "Contact support if problem persists",
                    ],
                    technical_details=str(e),
                ),
                no_update,
                no_update,
                {"display": "none"},
                no_update,
                no_update,
            )

        # Retry exhausted errors
        except RetryExhaustedError as e:
            processing_outcome = "retry_exhausted"
            logger.error(
                f"Retry exhausted: {str(e)}",
                extra={"file_name": file_data.get("filename", "unknown")},
            )

            return (
                create_processing_error_alert(
                    "Processing Failed After Retries",
                    "Processing failed after multiple retry attempts.",
                    error_type="RetryExhaustedError",
                    recovery_suggestions=[
                        "Check your network connection",
                        "Wait a moment and try again",
                        "Try with a smaller dataset",
                        "Contact support if problem persists",
                    ],
                    technical_details=str(e),
                ),
                no_update,
                no_update,
                {"display": "none"},
                no_update,
                no_update,
            )

        # Stage processing errors
        except StageProcessingError as e:
            processing_outcome = "stage_error"
            logger.error(
                f"Stage processing failed: {e.stage_name}",
                extra={
                    "file_name": file_data.get("filename", "unknown"),
                    "stage": e.stage_name,
                    "error": str(e.original_error),
                },
            )

            return (
                create_processing_error_alert(
                    f"Processing Failed: {e.stage_name}",
                    f"An error occurred during {e.stage_name}.",
                    error_type="StageProcessingError",
                    recovery_suggestions=[
                        "This stage may be temporarily unavailable",
                        "Wait a moment and try again",
                        "Try with different data",
                        "Contact support if problem persists",
                    ],
                    technical_details=str(e.original_error),
                ),
                no_update,
                no_update,
                {"display": "none"},
                no_update,
                no_update,
            )

        # Generic processing errors
        except ProcessingError as e:
            processing_outcome = "processing_error"
            logger.error(
                f"Processing error: {str(e)}",
                extra={"file_name": file_data.get("filename", "unknown")},
            )

            return (
                create_processing_error_alert(
                    "Processing Error",
                    "An error occurred while processing your data.",
                    error_type="ProcessingError",
                    recovery_suggestions=[
                        "Please try again",
                        "Try with the example dataset first",
                        "Check that your file follows the correct format",
                        "Contact support if problem persists",
                    ],
                    technical_details=str(e),
                ),
                no_update,
                no_update,
                {"display": "none"},
                no_update,
                no_update,
            )

        # Unexpected errors
        except Exception as e:
            processing_outcome = "unexpected_error"
            logger.exception(
                "Unexpected error during data processing",
                exc_info=True,
                extra={
                    "file_name": file_data.get("filename", "unknown"),
                    "sample_count": file_data.get("sample_count"),
                    "ko_count": file_data.get("ko_count"),
                    "error_type": type(e).__name__,
                },
            )

            return (
                create_processing_error_alert(
                    "Unexpected Error",
                    "An unexpected error occurred while processing.",
                    error_type=type(e).__name__,
                    recovery_suggestions=[
                        "Please try again",
                        "If using custom data, try the example dataset first",
                        "Check that your file follows the correct format",
                        "Contact support if problem persists",
                    ],
                    technical_details=str(e),
                ),
                no_update,
                no_update,
                {"display": "none"},
                no_update,
                no_update,
            )
        finally:
            _observe_processing_duration(callback_started_at, processing_outcome)

    # Callback to show progress panel immediately when button is clicked

    @app.callback(
        Output("processing-progress", "style", allow_duplicate=True),
        Input("process-data-btn", "n_clicks"),
        prevent_initial_call=True,
    )
    def show_progress_on_click(n_clicks):
        """
        Show progress panel immediately when process button is clicked.
        This ensures the panel appears BEFORE the background callback starts.

        Parameters
        ----------
        n_clicks : int
            Button click count

        Returns
        -------
        dict
            Style to show progress panel
        """
        if n_clicks:
            return {"display": "block"}
        return {"display": "none"}

    # Regular callback for enabling/disabling process button
    @app.callback(
        Output("process-data-btn", "disabled", allow_duplicate=True),
        [Input("upload-data-store", "data"), Input("example-data-store", "data")],
        prevent_initial_call=True,
    )
    def enable_process_button(upload_data, example_data):
        """
        Enable process button when data is available.

        Parameters
        ----------
        upload_data : dict
            Uploaded file data
        example_data : dict
            Example file data

        Returns
        -------
        bool
            True if button should be disabled
        """
        return not (upload_data or example_data)

    @app.callback(
        [
            Output("url", "pathname", allow_duplicate=True),
            Output("url", "hash", allow_duplicate=True),
        ],
        Input("view-results-btn", "n_clicks"),
        prevent_initial_call=True,
    )
    @instrument_callback("processing.navigate_to_results")
    def navigate_to_results(n_clicks):
        """
        Navigate to results page when completion button is clicked.

        Parameters
        ----------
        n_clicks : int
            Button click count

        Returns
        -------
        tuple[str, str]
            URL pathname for results route and cleared hash.
        """
        callback_started_at = time.perf_counter()
        if not n_clicks:
            raise PreventUpdate
        logger.info(
            "navigate_to_results callback triggered",
            extra={"n_clicks": int(n_clicks)},
        )
        results_transition_logger.info(
            "RESULTS_SERVER_CALLBACK_SAMPLE %s",
            json.dumps(
                {
                    "callback": "processing.navigate_to_results",
                    "route": "/results",
                    "duration_seconds": round(
                        max(time.perf_counter() - callback_started_at, 0.0),
                        6,
                    ),
                    "n_clicks": int(n_clicks),
                },
                sort_keys=True,
                separators=(",", ":"),
            ),
        )
        return app_path("/results"), ""

    logger.info("[OK] Real processing callbacks registered successfully")
    logger.info(
        "  - process_data_with_spinner: Background callback with simple spinner"
    )
    logger.info("  - show_progress_on_click: Show panel immediately on click")
    logger.info("  - enable_process_button: Button enable/disable")
    logger.info("  - navigate_to_results: Redirect to /results from completion panel")

register_real_upload_callbacks

register_real_upload_callbacks(app)

Register real upload callbacks.

Parameters:

Name Type Description Default
app Dash

Dash application instance

required
Source code in src/presentation/callbacks/real_upload_callbacks.py
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
def register_real_upload_callbacks(app):
    """
    Register real upload callbacks.

    Parameters
    ----------
    app : Dash
        Dash application instance
    """
    logger.info("=" * 60)
    logger.info("Registering REAL UPLOAD callbacks...")
    logger.info("=" * 60)

    @callback(
        [
            Output("upload-status", "children"),
            Output("upload-data-store", "data"),
            Output("file-info-display", "children"),
        ],
        Input("upload-component", "contents"),
        State("upload-component", "filename"),
        prevent_initial_call=True,
    )
    @instrument_callback("upload.handle_upload")
    def handle_upload(contents, filename):
        """
        Handle file upload with comprehensive validation.

        Performs server-side validation including:
        - File size limits
        - Encoding validation (UTF-8/latin-1)
        - Filename sanitization
        - Content format validation
        - Sample and KO count limits
        - Sample name sanitization

        Parameters
        ----------
        contents : str
            Base64 encoded file contents
        filename : str
            Original filename

        Returns
        -------
        tuple
            (status_alert, file_data, file_info_card)
        """
        if contents is None:
            raise PreventUpdate

        try:
            # ============================================================
            # STEP 1: Decode Base64
            # ============================================================
            try:
                _, content_string = contents.split(",")
                decoded = base64.b64decode(content_string)
            except (ValueError, Exception) as e:
                logger.error(f"Base64 decode failed: {e}")
                _record_upload_metrics("upload", "decode_failed")
                return (
                    create_error_alert(
                        "Invalid File Format",
                        "Unable to read file. Please upload a valid text file.",
                        suggestions=[
                            "Ensure file is in text format (.txt)",
                            "Check file is not corrupted",
                        ],
                    ),
                    no_update,
                    None,  # Clear file info display on error
                )

            # ============================================================
            # STEP 2: Validate File Size (SERVER-SIDE)
            # ============================================================
            is_valid, error_msg = ValidationService.validate_file_size(
                size_bytes=len(decoded), max_bytes=settings.UPLOAD_MAX_SIZE_BYTES
            )

            if not is_valid:
                logger.warning(f"File size validation failed: {error_msg}")
                _record_upload_metrics(
                    "upload",
                    "file_size_exceeded",
                    size_bytes=len(decoded),
                )
                return (
                    create_error_alert(
                        "File Size Exceeded",
                        error_msg,
                        suggestions=[
                            f"Reduce file size to under {settings.UPLOAD_MAX_SIZE_MB} MB",
                            "Remove unnecessary samples or KO entries",
                            "Split into multiple smaller files",
                        ],
                    ),
                    no_update,
                    None,  # Clear file info display on error
                )

            # ============================================================
            # STEP 3: Validate and Decode Encoding
            # ============================================================
            is_valid, file_content, error_msg = ValidationService.validate_encoding(
                decoded
            )

            if not is_valid:
                logger.error(f"Encoding validation failed: {error_msg}")
                _record_upload_metrics(
                    "upload",
                    "encoding_failed",
                    size_bytes=len(decoded),
                )
                return (
                    create_error_alert(
                        "Encoding Error",
                        error_msg,
                        suggestions=[
                            "Save file with UTF-8 encoding",
                            "Use a text editor that supports UTF-8",
                            "Check for special characters",
                        ],
                    ),
                    no_update,
                    None,  # Clear file info display on error
                )

            # ============================================================
            # STEP 4: Sanitize Filename
            # ============================================================
            safe_filename = SanitizationService.sanitize_filename(filename)

            # ============================================================
            # STEP 5: Comprehensive Content Validation
            # ============================================================
            is_valid, error_msg = ValidationService.validate_raw_input(file_content)

            if not is_valid:
                logger.warning(f"Content validation failed: {error_msg}")
                _record_upload_metrics(
                    "upload",
                    "content_invalid",
                    size_bytes=len(decoded),
                )
                return (
                    create_error_alert(
                        "Invalid File Format",
                        error_msg,
                        suggestions=[
                            "Check file format: lines starting with '>' for samples",
                            "Ensure KO IDs follow format: K + 5 digits (e.g., K00001)",
                            "See example file for reference",
                        ],
                    ),
                    no_update,
                    None,  # Clear file info display on error
                )

            # ============================================================
            # STEP 6: Count Samples and KOs for Limit Validation
            # ============================================================
            lines = file_content.strip().split("\n")
            sample_count = sum(1 for line in lines if line.startswith(">"))
            ko_count = sum(1 for line in lines if line.strip().startswith("K"))

            # ============================================================
            # STEP 7: Validate Sample Count Limit
            # ============================================================
            is_valid, error_msg = ValidationService.validate_sample_count(
                sample_count=sample_count, max_samples=settings.UPLOAD_SAMPLE_LIMIT
            )

            if not is_valid:
                logger.warning(f"Sample count limit exceeded: {error_msg}")
                _record_upload_metrics(
                    "upload",
                    "sample_limit_exceeded",
                    size_bytes=len(decoded),
                )
                return (
                    create_error_alert(
                        "Sample Limit Exceeded",
                        error_msg,
                        suggestions=[
                            f"Reduce to {settings.UPLOAD_SAMPLE_LIMIT} samples or fewer",
                            "Split dataset into multiple files",
                            "Remove duplicate or unnecessary samples",
                        ],
                    ),
                    no_update,
                    None,  # Clear file info display on error
                )

            # ============================================================
            # STEP 8: Validate KO Count Limit
            # ============================================================
            is_valid, error_msg = ValidationService.validate_ko_count(
                ko_count=ko_count, max_kos=settings.UPLOAD_KO_LIMIT
            )

            if not is_valid:
                logger.warning(f"KO count limit exceeded: {error_msg}")
                _record_upload_metrics(
                    "upload",
                    "ko_limit_exceeded",
                    size_bytes=len(decoded),
                )
                return (
                    create_error_alert(
                        "KO Entry Limit Exceeded",
                        error_msg,
                        suggestions=[
                            f"Reduce to {settings.UPLOAD_KO_LIMIT:,} KO entries or fewer",
                            "Remove duplicate KO entries",
                            "Split into multiple files",
                        ],
                    ),
                    no_update,
                    None,  # Clear file info display on error
                )

            # ============================================================
            # STEP 9: Sanitize Sample Names
            # ============================================================
            warnings = []
            for idx, line in enumerate(lines, 1):
                if line.startswith(">"):
                    sample_name = line[1:].strip()
                    is_valid, sanitized, error = (
                        SanitizationService.sanitize_sample_name(sample_name)
                    )
                    if not is_valid:
                        logger.warning(
                            f"Invalid sample name on line {idx}: {sample_name}"
                        )
                        _record_upload_metrics(
                            "upload",
                            "sample_name_invalid",
                            size_bytes=len(decoded),
                        )
                        return (
                            create_error_alert(
                                "Invalid Sample Name",
                                f"Line {idx}: {error}",
                                suggestions=[
                                    "Use only letters, numbers, underscore (_), dash (-), and dot (.)",
                                    "Example: Sample_001, Sample-2024.v1",
                                ],
                            ),
                            no_update,
                            None,  # Clear file info display on error
                        )

                    # Log if sanitization changed the name
                    if sanitized != sample_name:
                        warnings.append(
                            f"Line {idx}: Sample name '{sample_name}' sanitized"
                        )

            # ============================================================
            # STEP 10: All Validations Passed - Store Data
            # ============================================================
            file_data = {
                "content": file_content,
                "filename": safe_filename,
                "original_filename": filename,
                "sample_count": sample_count,
                "ko_count": ko_count,
                "file_size_bytes": len(decoded),
            }

            logger.info(
                f"File upload successful: {safe_filename}",
                extra={
                    "uploaded_file": safe_filename,
                    "samples": sample_count,
                    "kos": ko_count,
                    "size_bytes": len(decoded),
                    "warnings": len(warnings),
                },
            )
            _record_upload_metrics("upload", "success", size_bytes=len(decoded))

            # ============================================================
            # STEP 11: Create File Info Display (includes success state)
            # ============================================================
            file_info = create_file_info_card(
                filename=safe_filename,
                sample_count=sample_count,
                ko_count=ko_count,
                file_size_bytes=len(decoded),
                max_samples=settings.UPLOAD_SAMPLE_LIMIT,
                max_kos=settings.UPLOAD_KO_LIMIT,
                max_size_mb=settings.UPLOAD_MAX_SIZE_MB,
                warnings=warnings if warnings else None,
            )

            # Return: no status message (deprecated), only file info card
            return no_update, file_data, file_info

        except Exception as e:
            # Generic error - log full traceback, show generic message
            logger.exception(f"Unexpected error during upload: {e}", exc_info=True)
            _record_upload_metrics("upload", "unexpected_error")
            return (
                create_error_alert(
                    "Unexpected Error",
                    "An unexpected error occurred during upload.",
                    suggestions=[
                        "Please try again",
                        "Check file format and contents",
                        "Contact support if problem persists",
                    ],
                ),
                no_update,
                None,  # Clear file info display on error
            )

    @callback(
        [
            Output("example-data-store", "data"),
            Output("upload-status", "children", allow_duplicate=True),
            Output("file-info-display", "children", allow_duplicate=True),
        ],
        Input("load-example-btn", "n_clicks"),
        prevent_initial_call=True,
    )
    @instrument_callback("upload.load_example_data")
    def load_example_data(n_clicks):
        """
        Load example dataset from file.

        Loads the pre-configured example dataset and displays file information
        with statistics about samples and KO entries.

        Parameters
        ----------
        n_clicks : int
            Number of clicks on example button

        Returns
        -------
        tuple
            (example_data, status_message, file_info)

        Notes
        -----
        Uses feedback components for consistent UI styling.
        Logs all loading events with structured context.
        """
        if n_clicks is None:
            raise PreventUpdate

        try:
            # ============================================================
            # STEP 1: Locate Example File
            # ============================================================
            example_file = (
                Path(__file__).parent.parent.parent / "data" / "exemple_dataset.txt"
            )

            if not example_file.exists():
                logger.error(
                    f"Example dataset file not found: {example_file}",
                    extra={"expected_path": str(example_file)},
                )
                _record_upload_metrics("example", "file_missing")
                return (
                    no_update,
                    create_error_alert(
                        "Example File Not Found",
                        "The example dataset file is missing from the application.",
                        suggestions=[
                            "Contact support to restore example file",
                            "Upload your own dataset instead",
                        ],
                    ),
                    None,  # Clear file info display on error
                )

            # ============================================================
            # STEP 2: Load and Parse File
            # ============================================================
            try:
                with open(example_file, "r", encoding="utf-8") as f:
                    file_content = f.read()
            except UnicodeDecodeError:
                logger.error(
                    "Example dataset encoding error",
                    extra={"file_path": str(example_file)},
                )
                _record_upload_metrics("example", "encoding_failed")
                return (
                    no_update,
                    create_error_alert(
                        "Encoding Error",
                        "Unable to read example dataset due to encoding issues.",
                        suggestions=["Contact support to fix example file encoding"],
                    ),
                    None,  # Clear file info display on error
                )

            # ============================================================
            # STEP 3: Count Samples and KOs
            # ============================================================
            lines = file_content.strip().split("\n")
            sample_count = sum(1 for line in lines if line.startswith(">"))
            ko_count = sum(1 for line in lines if line.strip().startswith("K"))

            # ============================================================
            # STEP 4: Create Data Object
            # ============================================================
            example_data = {
                "content": file_content,
                "filename": "exemple_dataset.txt",
                "sample_count": sample_count,
                "ko_count": ko_count,
                "file_size_bytes": len(file_content.encode("utf-8")),
            }

            logger.info(
                "Example dataset loaded successfully",
                extra={
                    "example_file": "exemple_dataset.txt",
                    "samples": sample_count,
                    "kos": ko_count,
                    "size_bytes": example_data["file_size_bytes"],
                },
            )
            _record_upload_metrics(
                "example",
                "success",
                size_bytes=example_data["file_size_bytes"],
            )

            # ============================================================
            # STEP 5: Create File Info Display (includes success message)
            # ============================================================
            file_info = create_file_info_card(
                filename="exemple_dataset.txt",
                sample_count=sample_count,
                ko_count=ko_count,
                file_size_bytes=example_data["file_size_bytes"],
                max_samples=settings.UPLOAD_SAMPLE_LIMIT,
                max_kos=settings.UPLOAD_KO_LIMIT,
                max_size_mb=settings.UPLOAD_MAX_SIZE_MB,
                warnings=None,
            )

            # Return: no status message (deprecated), only file info card
            return example_data, no_update, file_info

        except Exception as e:
            # Generic error - log full details, show generic message
            logger.exception("Unexpected error loading example dataset", exc_info=True)
            _record_upload_metrics("example", "unexpected_error")
            return (
                no_update,
                create_error_alert(
                    "Unexpected Error",
                    "An unexpected error occurred while loading the example dataset.",
                    suggestions=[
                        "Try again",
                        "Upload your own dataset instead",
                        "Contact support if problem persists: biorempp@gmail.com",
                    ],
                ),
                None,  # Clear file info display on error
            )

    logger.info("[OK] Real upload callbacks registered successfully")
    logger.info("  - handle_file_upload: File upload handler")
    logger.info("  - load_example_data: Example data loader")

register_results_callbacks

register_results_callbacks(app)

Register callbacks for on-demand table rendering.

Parameters:

Name Type Description Default
app Dash

Dash application instance

required
Notes

Tables render on-demand when accordion is expanded. Uses accordion item_id as trigger.

Source code in src/presentation/callbacks/results_callbacks.py
def register_results_callbacks(app):
    """
    Register callbacks for on-demand table rendering.

    Parameters
    ----------
    app : Dash
        Dash application instance

    Notes
    -----
    Tables render on-demand when accordion is expanded.
    Uses accordion item_id as trigger.
    """
    logger.info("=" * 60)
    logger.info("Registering RESULTS callbacks (on-demand accordions)...")
    logger.info("=" * 60)

    @callback(
        Output("biorempp-container", "children"),
        Input("biorempp-accordion", "active_item"),
        State("merged-result-store", "data"),
        prevent_initial_call=True,
    )
    def render_biorempp_table(active_item, merged_data):
        """Render BioRemPP table when accordion opens."""
        if not active_item or not merged_data:
            raise PreventUpdate

        df = pd.DataFrame(merged_data.get("biorempp_df", []))
        return create_ag_grid_table(
            table_id="biorempp-table",
            data=df,
            title=None,
            page_size=50,
            card_wrapper=False,
        )

    @callback(
        Output("hadeg-container", "children"),
        Input("hadeg-accordion", "active_item"),
        State("merged-result-store", "data"),
        prevent_initial_call=True,
    )
    def render_hadeg_table(active_item, merged_data):
        """Render HADEG table when accordion opens."""
        if not active_item or not merged_data:
            raise PreventUpdate

        df = pd.DataFrame(merged_data.get("hadeg_df", []))
        return create_ag_grid_table(
            table_id="hadeg-table",
            data=df,
            title=None,
            page_size=50,
            card_wrapper=False,
        )

    @callback(
        Output("toxcsm-container", "children"),
        Input("toxcsm-accordion", "active_item"),
        State("merged-result-store", "data"),
        prevent_initial_call=True,
    )
    def render_toxcsm_table(active_item, merged_data):
        """Render ToxCSM table when accordion opens.

        Uses toxcsm_raw_df to display merged data (user's compounds + ToxCSM data + Sample column).
        This shows only the compounds that matched the user's input, in wide format (66 columns).
        """
        if not active_item or not merged_data:
            raise PreventUpdate

        # Use toxcsm_raw_df for table display (merged data with Sample column)
        toxcsm_data = merged_data.get("toxcsm_raw_df", [])
        logger.info(f"[DEBUG] ToxCSM table callback triggered")
        logger.info(f"  - toxcsm_raw_df available: {'toxcsm_raw_df' in merged_data}")
        logger.info(f"  - toxcsm_raw_df rows: {len(toxcsm_data)}")

        df = pd.DataFrame(toxcsm_data)
        logger.info(
            f"  - DataFrame created with {len(df)} rows, {len(df.columns) if not df.empty else 0} columns"
        )

        if not df.empty and "Sample" in df.columns:
            logger.info(
                f"  - Sample column present: {df['Sample'].nunique()} unique samples"
            )

        return create_ag_grid_table(
            table_id="toxcsm-table",
            data=df,
            title=None,
            page_size=50,
            card_wrapper=False,
        )

    @callback(
        Output("kegg-container", "children"),
        Input("kegg-accordion", "active_item"),
        State("merged-result-store", "data"),
        prevent_initial_call=True,
    )
    def render_kegg_table(active_item, merged_data):
        """Render KEGG table when accordion opens."""
        if not active_item or not merged_data:
            raise PreventUpdate

        df = pd.DataFrame(merged_data.get("kegg_df", []))
        return create_ag_grid_table(
            table_id="kegg-table", data=df, title=None, page_size=50, card_wrapper=False
        )

    logger.info("[OK] Results callbacks registered successfully")
    logger.info("  - Tables render when accordion expands")
    logger.info("  - 4 AG Grid tables: BioRemPP, HADEG, ToxCSM, KEGG")
    logger.info("  - Built-in filters, sorting, and pagination enabled")

register_results_workflow_modal_callbacks

register_results_workflow_modal_callbacks(app) -> None

Register global workflow modal callback used by all UC Methods buttons.

Source code in src/presentation/callbacks/results_workflow_modal_callbacks.py
def register_results_workflow_modal_callbacks(app) -> None:
    """Register global workflow modal callback used by all UC Methods buttons."""

    @app.callback(
        [
            Output("results-workflow-modal", "is_open"),
            Output("results-workflow-modal-title", "children"),
            Output("results-workflow-modal-body", "children"),
        ],
        [
            Input({"type": "results-methods-link", "index": ALL}, "n_clicks"),
            Input("results-workflow-modal-close", "n_clicks"),
        ],
        State("url", "pathname"),
        prevent_initial_call=True,
    )
    def open_results_workflow_modal(_, close_n_clicks, pathname):
        """
        Open/close global workflow modal for UC Methods actions on /results.

        Parameters
        ----------
        _ : list[int | None]
            Click counts from all UC Methods buttons in /results layouts.
        close_n_clicks : int | None
            Close button clicks from global modal footer.
        pathname : str | None
            Current URL pathname.
        """
        return resolve_results_workflow_modal_update(
            trigger=ctx.triggered_id,
            trigger_value=(ctx.triggered[0]["value"] if ctx.triggered else None),
            close_n_clicks=close_n_clicks,
            pathname=pathname,
        )

register_upload_callbacks

register_upload_callbacks(app, upload_handler: Optional[UploadHandler] = None)

Register upload callbacks.

Parameters:

Name Type Description Default
app Dash

Dash application instance

required
upload_handler Optional[UploadHandler]

UploadHandler instance from Application Layer, by default None If None, creates new instance

None
Notes

DESATIVADO - Usando real_upload_callbacks.py

Este arquivo contém callbacks da Application Layer que foram substituídos pelos callbacks reais.

Source code in src/presentation/callbacks/upload_callbacks.py
def register_upload_callbacks(app, upload_handler: Optional[UploadHandler] = None):
    """
    Register upload callbacks.

    Parameters
    ----------
    app : Dash
        Dash application instance
    upload_handler : Optional[UploadHandler], optional
        UploadHandler instance from Application Layer, by default None
        If None, creates new instance

    Notes
    -----
    DESATIVADO - Usando real_upload_callbacks.py

    Este arquivo contém callbacks da Application Layer que foram
    substituídos pelos callbacks reais.
    """
    # DESATIVADO - Callbacks duplicados, usando real_upload_callbacks.py
    return

    # CALLBACK DESATIVADO - Conflita com real_upload_callbacks.py
    # @callback(
    #     [
    #         Output("upload-result-store", "data"),
    #         Output("upload-data-filename", "children")
    #     ],
    #     [
    #         Input("upload-data", "contents"),
    #         Input("load-sample-btn", "n_clicks")
    #     ],
    #     [
    #         State("upload-data", "filename")
    #     ],
    #     prevent_initial_call=True
    # )
    def process_upload_disabled(
        contents: Optional[str], sample_clicks: Optional[int], filename: Optional[str]
    ) -> Tuple[Optional[Dict[str, Any]], str]:
        """
        Process file upload or load sample data.

        Parameters
        ----------
        contents : Optional[str]
            Base64 encoded file contents
        sample_clicks : Optional[int]
            Number of clicks on sample data button
        filename : Optional[str]
            Uploaded filename

        Returns
        -------
        Tuple[Optional[Dict[str, Any]], str]
            - UploadResultDTO as dict
            - Filename display text

        Notes
        -----
        - Triggered by upload or sample button click
        - Decodes base64 content
        - Calls UploadHandler.process_upload()
        - Returns DTO for validation panel
        """
        triggered_id = ctx.triggered_id

        # Handle sample data button
        if triggered_id == "load-sample-btn":
            try:
                # Load sample data file
                sample_path = Path("data/sample_data.txt")
                if not sample_path.exists():
                    sample_path = Path("biorempp_web/data/sample_data.txt")

                with open(sample_path, "r") as f:
                    sample_content = f.read()

                # Process sample data
                result = upload_handler.process_upload(
                    file_content=sample_content, filename="sample_data.txt"
                )

                filename_msg = "[OK] Sample data loaded: sample_data.txt"
                return result.to_dict(), filename_msg

            except Exception as e:
                # Return error result
                error_result = {
                    "success": False,
                    "sample_count": 0,
                    "ko_count": 0,
                    "validation_errors": [f"Failed to load sample data: {str(e)}"],
                    "validation_warnings": [],
                    "filename": "sample_data.txt",
                }
                return error_result, "✗ Error loading sample data"

        # Handle file upload
        if triggered_id == "upload-data" and contents:
            try:
                # Decode base64 content
                content_type, content_string = contents.split(",")
                decoded = base64.b64decode(content_string)
                file_content = decoded.decode("utf-8")

                # Process upload
                result = upload_handler.process_upload(
                    file_content=file_content, filename=filename
                )

                return result.to_dict(), f"[OK] File uploaded: {filename}"

            except Exception as e:
                # Return error result
                error_result = {
                    "success": False,
                    "sample_count": 0,
                    "ko_count": 0,
                    "validation_errors": [f"Failed to process file: {str(e)}"],
                    "validation_warnings": [],
                    "filename": filename or "unknown",
                }
                return error_result, "✗ Error processing file"

        raise PreventUpdate

    # CALLBACK DESATIVADO - Conflita com real_upload_callbacks.py
    # @callback(
    #     [
    #         Output("validation-panel", "style"),
    #         Output("validation-panel", "children")
    #     ],
    #     [Input("upload-result-store", "data")],
    #     prevent_initial_call=True
    # )
    def show_validation_disabled(
        upload_result: Optional[Dict[str, Any]]
    ) -> Tuple[Dict, Any]:
        """
        Show validation panel with results.

        Parameters
        ----------
        upload_result : Optional[Dict[str, Any]]
            UploadResultDTO data from store

        Returns
        -------
        Tuple[Dict, Any]
            - Style dict (display: block/none)
            - Validation panel component

        Notes
        -----
        - Creates validation_panel with upload results
        - Makes panel visible
        - Shows success/error alerts and statistics
        """
        if not upload_result:
            return {"display": "none"}, no_update

        # Import here to avoid circular dependency
        from ..components.composite import create_validation_panel

        panel = create_validation_panel(validation_data=upload_result)

        return {"display": "block"}, panel.children

register_all_callbacks

register_all_callbacks(app, plot_service=None, upload_handler=None, data_processor=None, progress_tracker=None)

Register all application callbacks.

Parameters:

Name Type Description Default
app Dash

Dash application instance

required
plot_service Optional[PlotService]

Singleton PlotService instance (CRITICAL: shared across all callbacks)

None
upload_handler Optional[UploadHandler]

UploadHandler from Application Layer

None
data_processor Optional[DataProcessor]

DataProcessor from Application Layer

None
progress_tracker Optional[ProgressTracker]

ProgressTracker from Application Layer

None
Notes
  • Registers upload callbacks (file upload, validation)
  • Registers processing callbacks (data processing, progress)
  • Services can be injected or auto-initialized
  • Falls back to real callbacks if Application Layer not available
  • PlotService is passed to all module callbacks (Singleton pattern)

Examples:

>>> from dash import Dash
>>> from application.plot_services.singleton import get_plot_service
>>> app = Dash(__name__)
>>> plot_service = get_plot_service()
>>> register_all_callbacks(app, plot_service=plot_service)
Source code in src/presentation/callbacks/__init__.py
def register_all_callbacks(
    app,
    plot_service=None,
    upload_handler=None,
    data_processor=None,
    progress_tracker=None,
):
    """
    Register all application callbacks.

    Parameters
    ----------
    app : Dash
        Dash application instance
    plot_service : Optional[PlotService]
        Singleton PlotService instance (CRITICAL: shared across all callbacks)
    upload_handler : Optional[UploadHandler]
        UploadHandler from Application Layer
    data_processor : Optional[DataProcessor]
        DataProcessor from Application Layer
    progress_tracker : Optional[ProgressTracker]
        ProgressTracker from Application Layer

    Notes
    -----
    - Registers upload callbacks (file upload, validation)
    - Registers processing callbacks (data processing, progress)
    - Services can be injected or auto-initialized
    - Falls back to real callbacks if Application Layer not available
    - PlotService is passed to all module callbacks (Singleton pattern)

    Examples
    --------
    >>> from dash import Dash
    >>> from application.plot_services.singleton import get_plot_service
    >>> app = Dash(__name__)
    >>> plot_service = get_plot_service()
    >>> register_all_callbacks(app, plot_service=plot_service)
    """
    logger.info("\n" + "=" * 80)
    logger.info("REGISTERING ALL CALLBACKS")
    logger.info("=" * 80)

    # Always register real callbacks (they work standalone)
    logger.info("\n[1/14] Registering REAL UPLOAD callbacks...")
    register_real_upload_callbacks(app)

    logger.info("\n[2/14] Registering REAL PROCESSING callbacks...")
    register_real_processing_callbacks(app)

    logger.info("\n[3/14] Registering JOB RESUME callbacks...")
    register_job_resume_callbacks(app)

    logger.info("\n[4/14] Registering RESULTS callbacks...")
    register_results_callbacks(app)

    logger.info("\n[5/14] Registering RESULTS WORKFLOW MODAL callbacks...")
    register_results_workflow_modal_callbacks(app)

    logger.info("\n[6/14] Registering MODULE 1 callbacks...")
    register_module1_callbacks(app, plot_service)

    logger.info("\n[7/14] Registering MODULE 2 callbacks...")
    register_module2_callbacks(app, plot_service)

    logger.info("\n[8/14] Registering MODULE 3 callbacks...")
    register_module3_callbacks(app, plot_service)

    logger.info("\n[9/14] Registering MODULE 4 callbacks...")
    register_module4_callbacks(app, plot_service)

    logger.info("\n[10/14] Registering MODULE 5 callbacks...")
    register_module5_callbacks(app, plot_service)

    logger.info("\n[11/14] Registering MODULE 6 callbacks...")
    register_module6_callbacks(app, plot_service)

    logger.info("\n[12/14] Registering MODULE 7 callbacks...")
    register_module7_callbacks(app, plot_service)

    logger.info("\n[13/14] Registering MODULE 8 callbacks...")
    register_module8_callbacks(app, plot_service)

    logger.info("\n[14/14] Registering INFO MODAL callbacks...")
    register_info_modal_callbacks(app)

    logger.info("\n" + "=" * 80)
    logger.info("[OK] ALL CALLBACKS REGISTERED SUCCESSFULLY")
    logger.info("=" * 80 + "\n")

:::

Callbacks Layer

The Callbacks Layer implements Dash callback handlers that connect UI events to application logic, providing real-time interactivity for the BioRemPP platform.

Callbacks Layer

The Callbacks Layer implements Dash callback handlers that connect UI events to application logic, providing real-time interactivity for the BioRemPP platform.


Overview

Callbacks are the heart of the Dash application, handling user interactions and updating the UI dynamically. The BioRemPP callbacks layer follows a modular architecture organized by functional areas.

Responsibilities

  • Event Handling: Respond to user interactions (clicks, file uploads, selections)
  • State Management: Update Dash stores and component properties
  • Application Integration: Call Application Layer services and handlers
  • Progress Tracking: Provide real-time feedback during long-running operations
  • Navigation: Manage page navigation and routing

Architecture

Callback Structure

presentation/callbacks/
├── Core Callbacks (Global)
│   ├── upload_callbacks.py         # File upload handling
│   ├── processing_callbacks.py     # Data processing workflow
│   ├── navigation_callbacks.py     # Navigation and routing
│   ├── results_callbacks.py        # Results display
│   └── real_*.py                   # Real-data integration callbacks
├── Module Orchestrators
│   └── module_callbacks/
│       ├── module1_callbacks.py    # Module 1 orchestrator
│       ├── module2_callbacks.py    # Module 2 orchestrator
│       ├── ...
│       └── module8_callbacks.py    # Module 8 orchestrator
└── Use Case Callbacks (Specific)
    ├── module1/
    │   ├── uc_1_1_callbacks.py     # Database overlap analysis
    │   ├── uc_1_2_callbacks.py     # Regulatory overlap
    │   ├── ...
    │   └── uc_1_6_callbacks.py     # Sample-agency heatmap
    ├── module2/
    │   └── ...
    └── module3-8/
        └── ...

Data Flow

graph TD
    A[User Interaction] --> B[Dash Callback]
    B --> C{Callback Type}

    C -->|Upload| D[UploadHandler]
    C -->|Processing| E[DataProcessor]
    C -->|Analysis| F[AnalysisOrchestrator]
    C -->|Navigation| G[NavigationHandler]

    D --> H[Update Stores]
    E --> H
    F --> H
    G --> H

    H --> I[Update UI Components]
    I --> J[User Sees Result]

Core Callbacks

Upload Callbacks

Handles file upload workflow and sample data validation.

Key Functions: - File upload and parsing - Sample data validation - Upload result storage - Validation feedback display

Integration: Uses UploadHandler from Application Layer.

Stores Updated: - upload-result-store: Contains UploadResultDTO - sample-data-store: Parsed sample data - validation-feedback-store: Validation messages


Processing Callbacks

Manages data processing workflow with real-time progress tracking.

Key Functions: - Start/stop processing - Progress updates - Processing result storage - Completion notifications

Integration: Uses DataProcessor and ProgressTracker from Application Layer.

Stores Updated: - processing-progress-store: Progress percentage and status - merged-data-store: Processed merged data - processing-status-store: Success/error state


Controls application navigation and scroll behavior.

Key Functions: - Toggle offcanvas navigation - Scroll to target sections - Update URL hash - Handle deep linking

Stores Updated: - url-store: Current URL and hash - offcanvas-state-store: Open/closed state


Results Callbacks

Displays analysis results and visualizations.

Key Functions: - Render result tables - Display plot figures - Export data triggers - Result filtering

Integration: Uses ResultExporter from Application Layer.

Stores Updated: - results-display-store: Formatted results - export-status-store: Export completion status