From 8674a5a0f7a20b5afb287fe49b76fbefcb4fb03d Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Tue, 21 Apr 2026 16:24:17 +0530 Subject: [PATCH 1/7] fix: run error handling, rollback visibility, stale status indicator, and spurious notifications Bug 1: Generic "Check server logs" error on model run failure - execute_run_command caught all exceptions with a blanket `except Exception` and returned "Model execution failed. Check server logs for details." - Now catches VisitranBaseExceptions/VisitranBackendBaseException separately and returns the detailed error via err.error_response() (e.g. model name, actual error cause, remediation hint) - Generic Exception fallback now returns 500 with a clearer message Bug 2: Rollback button never appearing on run failure - execute_visitran_run_command sets is_rollback on the exception's error_args, but error_response() only exposed it nested inside message_args - Frontend checks error.response.data.is_rollback (top level) - Fixed both VisitranBaseExceptions and VisitranBackendBaseException error_response() to surface is_rollback at the top level when present Bug 3: Stale failure indicator in explorer after fixing model - run_status and failure_reason on ConfigModels were only updated during execute_graph and never cleared when the user modified the model spec - After deleting a bad transformation, the explorer still showed the old red dot with the previous error - Added _reset_model_run_status in NoCodeModel._validate_and_update_model to clear FAILED status back to NOT_STARTED when the spec changes Bug 4: "Something went wrong" popup on frontend console errors - .catch(error => notify({ error })) catches both API errors and JS runtime errors (e.g. TypeError from unexpected response shape) - JS runtime errors have no error.response, so notification-service fell through to the generic "Something went wrong" description - Now skips the popup for non-API errors and logs them to console instead Co-Authored-By: Claude Opus 4.6 (1M context) --- .../application/context/no_code_model.py | 21 ++++++++++++++++++- backend/backend/core/routers/execute/views.py | 11 +++++++--- .../visitran_backend_base_exceptions.py | 5 ++++- backend/visitran/errors/base_exceptions.py | 5 ++++- frontend/src/service/notification-service.js | 7 +++++++ 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/backend/backend/application/context/no_code_model.py b/backend/backend/application/context/no_code_model.py index 1d49e607..8d673e3e 100644 --- a/backend/backend/application/context/no_code_model.py +++ b/backend/backend/application/context/no_code_model.py @@ -3,6 +3,7 @@ from typing import Any from backend.application.context.application import ApplicationContext +from backend.core.models.config_models import ConfigModels from backend.errors import InvalidModelConfigError @@ -10,6 +11,21 @@ class NoCodeModel(ApplicationContext): def __init__(self, project_id: str, environment_id: str = "") -> None: super().__init__(project_id, environment_id) + def _reset_model_run_status(self, model_name: str) -> None: + """Reset run status when the model spec changes so the explorer + doesn't keep showing a stale failure indicator.""" + try: + model_instance = ConfigModels.objects.get( + project_instance__project_uuid=self.session.project_id, + model_name=model_name, + ) + if model_instance.run_status == ConfigModels.RunStatus.FAILED: + model_instance.run_status = ConfigModels.RunStatus.NOT_STARTED + model_instance.failure_reason = None + model_instance.save(update_fields=["run_status", "failure_reason"]) + except ConfigModels.DoesNotExist: + pass + def _validate_and_update_model( self, model_data: dict[str, Any], @@ -49,7 +65,10 @@ def _validate_and_update_model( config_type=config_type ) # Converting the current model to python. - return self.update_model(model_name=model_name, model_data=model_data) + result = self.update_model(model_name=model_name, model_data=model_data) + # Reset stale run status so the explorer doesn't show the previous error + self._reset_model_run_status(model_name) + return result def set_model_config_and_reference(self, no_code_data: dict[str, Any], model_name: str): """ diff --git a/backend/backend/core/routers/execute/views.py b/backend/backend/core/routers/execute/views.py index eb30a716..e074a262 100644 --- a/backend/backend/core/routers/execute/views.py +++ b/backend/backend/core/routers/execute/views.py @@ -7,9 +7,11 @@ from rest_framework.request import Request from rest_framework.response import Response from visitran.singleton import Singleton +from visitran.errors.base_exceptions import VisitranBaseExceptions import logging from backend.application.context.application import ApplicationContext +from backend.errors.visitran_backend_base_exceptions import VisitranBackendBaseException from backend.core.utils import handle_http_request, sanitize_data from backend.utils.cache_service.decorators.cache_decorator import clear_cache from backend.utils.constants import HTTPMethods @@ -63,10 +65,13 @@ def execute_run_command(request: Request, project_id: str) -> Response: logger.info(f"[execute_run_command] Completed successfully for file_name={file_name}") _data = {"status": "success"} return Response(data=_data) - except Exception: + except (VisitranBaseExceptions, VisitranBackendBaseException) as err: logger.exception(f"[execute_run_command] DAG execution failed for file_name={file_name}") - _data = {"status": "failed", "error_message": "Model execution failed. Check server logs for details."} - return Response(data=_data, status=status.HTTP_400_BAD_REQUEST) + return Response(data=err.error_response(), status=status.HTTP_400_BAD_REQUEST) + except Exception: + logger.exception(f"[execute_run_command] Unexpected error during DAG execution for file_name={file_name}") + _data = {"status": "failed", "error_message": "An unexpected error occurred while executing the model. Please try again or contact support if the issue persists."} + return Response(data=_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR) finally: app.visitran_context.close_db_connection() diff --git a/backend/backend/errors/visitran_backend_base_exceptions.py b/backend/backend/errors/visitran_backend_base_exceptions.py index 0e7ef4cc..a7d64ac0 100644 --- a/backend/backend/errors/visitran_backend_base_exceptions.py +++ b/backend/backend/errors/visitran_backend_base_exceptions.py @@ -57,7 +57,7 @@ def severity(self) -> str: return "Error" def error_response(self) -> dict[str, Any]: - return { + response = { "status": "failed", "class": self.__class__.__name__, "error_message": self.error_message, @@ -65,3 +65,6 @@ def error_response(self) -> dict[str, Any]: "severity": self.severity, "is_markdown": self._is_markdown, } + if "is_rollback" in self._msg_args: + response["is_rollback"] = self._msg_args["is_rollback"] + return response diff --git a/backend/visitran/errors/base_exceptions.py b/backend/visitran/errors/base_exceptions.py index f7bac007..e86e6740 100644 --- a/backend/visitran/errors/base_exceptions.py +++ b/backend/visitran/errors/base_exceptions.py @@ -37,10 +37,13 @@ def __str__(self) -> str: return self._error_msg def error_response(self) -> dict[str, Any]: - return { + response = { "status": "failed", "error_message": self.error_message, "message_args": self._msg_args, "severity": self.severity, "is_markdown": self._is_markdown, } + if "is_rollback" in self._msg_args: + response["is_rollback"] = self._msg_args["is_rollback"] + return response diff --git a/frontend/src/service/notification-service.js b/frontend/src/service/notification-service.js index 68afbe6f..09942871 100644 --- a/frontend/src/service/notification-service.js +++ b/frontend/src/service/notification-service.js @@ -59,6 +59,13 @@ function NotificationProvider({ children }) { return; } + // Skip notification for JS runtime errors (not API failures) + // These have no useful info for the user — log them for debugging instead + if (error && !error.response && !message && !description) { + console.error("[Runtime Error]", error); + return; + } + const errorStatus = error?.response?.status; const errorData = error?.response?.data; if (errorData && typeof errorData.error_message === "string") { From 3a7da9dd84862ba1e730e3af11b78b5fff93ee57 Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Tue, 21 Apr 2026 16:30:22 +0530 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20address=20greptile=20review=20?= =?UTF-8?q?=E2=80=94=20network=20errors=20and=20status=20code=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add !error.request check so Axios network errors (connection refused, server down) still show notifications — only suppress true JS runtime errors that have neither .response nor .request - Use err.to_response() for VisitranBackendBaseException to preserve the exception's own HTTP status code (403, 404, etc.) instead of hardcoding 400 Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/backend/core/routers/execute/views.py | 2 ++ frontend/src/service/notification-service.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/backend/core/routers/execute/views.py b/backend/backend/core/routers/execute/views.py index e074a262..73995d72 100644 --- a/backend/backend/core/routers/execute/views.py +++ b/backend/backend/core/routers/execute/views.py @@ -67,6 +67,8 @@ def execute_run_command(request: Request, project_id: str) -> Response: return Response(data=_data) except (VisitranBaseExceptions, VisitranBackendBaseException) as err: logger.exception(f"[execute_run_command] DAG execution failed for file_name={file_name}") + if hasattr(err, 'to_response'): + return err.to_response() return Response(data=err.error_response(), status=status.HTTP_400_BAD_REQUEST) except Exception: logger.exception(f"[execute_run_command] Unexpected error during DAG execution for file_name={file_name}") diff --git a/frontend/src/service/notification-service.js b/frontend/src/service/notification-service.js index 09942871..834af827 100644 --- a/frontend/src/service/notification-service.js +++ b/frontend/src/service/notification-service.js @@ -61,7 +61,7 @@ function NotificationProvider({ children }) { // Skip notification for JS runtime errors (not API failures) // These have no useful info for the user — log them for debugging instead - if (error && !error.response && !message && !description) { + if (error && !error.response && !error.request && !message && !description) { console.error("[Runtime Error]", error); return; } From ca162d4f5590f71d18ad167fef65378c2ff7ce58 Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Tue, 21 Apr 2026 16:34:18 +0530 Subject: [PATCH 3/7] fix: prettier formatting for multi-condition guard Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/service/notification-service.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/service/notification-service.js b/frontend/src/service/notification-service.js index 834af827..c20bb7e9 100644 --- a/frontend/src/service/notification-service.js +++ b/frontend/src/service/notification-service.js @@ -61,7 +61,13 @@ function NotificationProvider({ children }) { // Skip notification for JS runtime errors (not API failures) // These have no useful info for the user — log them for debugging instead - if (error && !error.response && !error.request && !message && !description) { + if ( + error && + !error.response && + !error.request && + !message && + !description + ) { console.error("[Runtime Error]", error); return; } From 4d4a12b631d5dda06d0de9023085a0d1166aa833 Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Tue, 21 Apr 2026 16:35:14 +0530 Subject: [PATCH 4/7] fix: skip run status reset for presentation-only changes Presentation changes (column sort/hide/order) don't affect model execution, so they should not clear a FAILED status indicator. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/backend/application/context/no_code_model.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/backend/application/context/no_code_model.py b/backend/backend/application/context/no_code_model.py index 8d673e3e..6f5bdabd 100644 --- a/backend/backend/application/context/no_code_model.py +++ b/backend/backend/application/context/no_code_model.py @@ -66,8 +66,9 @@ def _validate_and_update_model( ) # Converting the current model to python. result = self.update_model(model_name=model_name, model_data=model_data) - # Reset stale run status so the explorer doesn't show the previous error - self._reset_model_run_status(model_name) + # Only reset stale run status for changes that affect model execution + if config_type not in ("presentation",): + self._reset_model_run_status(model_name) return result def set_model_config_and_reference(self, no_code_data: dict[str, Any], model_name: str): From 2f24d88b65647442e7a7567205a54b837f0ec038 Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Tue, 21 Apr 2026 17:17:27 +0530 Subject: [PATCH 5/7] refactor: extract NON_EXECUTION_CONFIG_TYPES constant Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/backend/application/context/no_code_model.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/backend/application/context/no_code_model.py b/backend/backend/application/context/no_code_model.py index 6f5bdabd..825a518e 100644 --- a/backend/backend/application/context/no_code_model.py +++ b/backend/backend/application/context/no_code_model.py @@ -7,6 +7,9 @@ from backend.errors import InvalidModelConfigError +NON_EXECUTION_CONFIG_TYPES = {"presentation"} + + class NoCodeModel(ApplicationContext): def __init__(self, project_id: str, environment_id: str = "") -> None: super().__init__(project_id, environment_id) @@ -67,7 +70,7 @@ def _validate_and_update_model( # Converting the current model to python. result = self.update_model(model_name=model_name, model_data=model_data) # Only reset stale run status for changes that affect model execution - if config_type not in ("presentation",): + if config_type not in NON_EXECUTION_CONFIG_TYPES: self._reset_model_run_status(model_name) return result From fe74614c604d440f94f68cbe7c2392c55d1e70d8 Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Tue, 21 Apr 2026 18:18:31 +0530 Subject: [PATCH 6/7] =?UTF-8?q?revert:=20remove=20FE=20notification=20guar?= =?UTF-8?q?d=20=E2=80=94=20to=20be=20handled=20separately?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/service/notification-service.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/frontend/src/service/notification-service.js b/frontend/src/service/notification-service.js index c20bb7e9..68afbe6f 100644 --- a/frontend/src/service/notification-service.js +++ b/frontend/src/service/notification-service.js @@ -59,19 +59,6 @@ function NotificationProvider({ children }) { return; } - // Skip notification for JS runtime errors (not API failures) - // These have no useful info for the user — log them for debugging instead - if ( - error && - !error.response && - !error.request && - !message && - !description - ) { - console.error("[Runtime Error]", error); - return; - } - const errorStatus = error?.response?.status; const errorData = error?.response?.data; if (errorData && typeof errorData.error_message === "string") { From 4a61814f1f3ef83e3d01853b4ca0463f80ff99c3 Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Tue, 21 Apr 2026 18:20:56 +0530 Subject: [PATCH 7/7] fix: catch all exceptions in _reset_model_run_status to prevent cascade A DB error during status reset should not fail the outer model update. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/backend/application/context/no_code_model.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/backend/application/context/no_code_model.py b/backend/backend/application/context/no_code_model.py index 825a518e..9b8e354b 100644 --- a/backend/backend/application/context/no_code_model.py +++ b/backend/backend/application/context/no_code_model.py @@ -28,6 +28,11 @@ def _reset_model_run_status(self, model_name: str) -> None: model_instance.save(update_fields=["run_status", "failure_reason"]) except ConfigModels.DoesNotExist: pass + except Exception: + logging.warning( + f"[_reset_model_run_status] Failed to reset run status for model={model_name}", + exc_info=True, + ) def _validate_and_update_model( self,