Private
Public Access
refactor(app_controller): migrate 4 remaining helper sites to Result (Phase 6 Group 6.7 final)
Migrates the final 4 silent-swallow sites: - tool_calls json serialization (cb_load_prior_log) via _serialize_tool_calls_result - queue_fallback bounded retry (Pattern 5 drain) via _run_pending_tasks_once_result - _refresh_from_project.active_track deserialize via _deserialize_active_track_result - _flush_to_project (FR1 guard) via _flush_to_project_result Audit gate: INTERNAL_SILENT_SWALLOW for src/app_controller.py: 4 -> 0. Per-site count = 0 (Phase 6 hard gate satisfied).
This commit is contained in:
+212
-77
@@ -1844,17 +1844,34 @@ class AppController:
|
|||||||
tasks = self._pending_gui_tasks[:]
|
tasks = self._pending_gui_tasks[:]
|
||||||
self._pending_gui_tasks.clear()
|
self._pending_gui_tasks.clear()
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
try:
|
result = self._execute_gui_task_result(task)
|
||||||
action = task.get("action") or task.get("type")
|
if not result.ok:
|
||||||
if action:
|
err = result.errors[0]
|
||||||
session_logger.log_api_hook("PROCESS_TASK", action, str(task))
|
action = task.get('action') or task.get('type') or 'unknown'
|
||||||
if action in self._gui_task_handlers:
|
self._last_request_errors.append((f"gui_task[{action}]", err))
|
||||||
self._gui_task_handlers[action](self, task)
|
print(f"Error executing GUI task ({action}): {err.message}")
|
||||||
except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, RuntimeError) as e:
|
|
||||||
logging.getLogger(__name__).debug("GUI task execution failed: %s", e, extra={"source": "app_controller._process_pending_gui_tasks"})
|
|
||||||
print(f"Error executing GUI task ({task.get('action') or task.get('type')}): {e}")
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def _execute_gui_task_result(self, task: dict) -> "Result[None]":
|
||||||
|
"""Phase 6 Group 6.6: per-task execution with Result propagation.
|
||||||
|
On failure: any handler exception -> ErrorInfo(original=e).
|
||||||
|
Caller (`_process_pending_gui_tasks`) appends to
|
||||||
|
`self._last_request_errors` for sub-track 4 GUI."""
|
||||||
|
try:
|
||||||
|
action = task.get("action") or task.get("type")
|
||||||
|
if action:
|
||||||
|
session_logger.log_api_hook("PROCESS_TASK", action, str(task))
|
||||||
|
if action in self._gui_task_handlers:
|
||||||
|
self._gui_task_handlers[action](self, task)
|
||||||
|
return OK
|
||||||
|
except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, RuntimeError) as e:
|
||||||
|
return Result(data=None, errors=[ErrorInfo(
|
||||||
|
kind=ErrorKind.INTERNAL,
|
||||||
|
message=str(e),
|
||||||
|
source=f"app_controller._execute_gui_task_result[{task.get('action') or task.get('type')}]",
|
||||||
|
original=e,
|
||||||
|
)])
|
||||||
|
|
||||||
def _process_pending_history_adds(self) -> None:
|
def _process_pending_history_adds(self) -> None:
|
||||||
"""
|
"""
|
||||||
Synchronizes pending history entries to the active discussion and project state.
|
Synchronizes pending history entries to the active discussion and project state.
|
||||||
@@ -2126,15 +2143,98 @@ class AppController:
|
|||||||
]
|
]
|
||||||
for p in paths_to_check:
|
for p in paths_to_check:
|
||||||
if p.exists():
|
if p.exists():
|
||||||
try:
|
result = self._read_ref_file_result(p)
|
||||||
with open(p, "r", encoding="utf-8") as rf:
|
if result.ok:
|
||||||
return rf.read()
|
return result.data
|
||||||
except (OSError, IOError, UnicodeDecodeError) as e:
|
self._last_request_errors.append((f"ref_file_read[{ref_file}]", result.errors[0]))
|
||||||
logging.getLogger(__name__).debug("ref file read failed for %s: %s", p, e, extra={"source": "app_controller.cb_load_prior_log._resolve_log_ref"})
|
return f"[ERROR READING REF: {ref_file}]"
|
||||||
return f"[ERROR READING REF: {ref_file}]"
|
|
||||||
return match.group(0)
|
return match.group(0)
|
||||||
return re.sub(pattern, replace_ref, content)
|
return re.sub(pattern, replace_ref, content)
|
||||||
|
|
||||||
|
def _read_ref_file_result(self, p: Path) -> "Result[str]":
|
||||||
|
"""Phase 6 Group 6.7: read a [REF:...] file content.
|
||||||
|
On failure: OSError/IOError/UnicodeDecodeError -> ErrorInfo(original=e).
|
||||||
|
Caller (`_resolve_log_ref`) appends to `self._last_request_errors`."""
|
||||||
|
try:
|
||||||
|
with open(p, "r", encoding="utf-8") as rf:
|
||||||
|
return Result(data=rf.read())
|
||||||
|
except (OSError, IOError, UnicodeDecodeError) as e:
|
||||||
|
return Result(data="", errors=[ErrorInfo(
|
||||||
|
kind=ErrorKind.INTERNAL,
|
||||||
|
message=str(e),
|
||||||
|
source=f"app_controller._read_ref_file_result[{p.name}]",
|
||||||
|
original=e,
|
||||||
|
)])
|
||||||
|
|
||||||
|
def _flush_to_project_result(self, cleaned_proj: dict, path: str) -> "Result[None]":
|
||||||
|
"""Phase 6 Group 6.7: flush to project file with Result propagation.
|
||||||
|
On failure: OSError/IOError/PermissionError/RuntimeError -> ErrorInfo(original=e).
|
||||||
|
Caller (`_flush_to_project`) appends to `self._last_request_errors`."""
|
||||||
|
try:
|
||||||
|
project_manager.save_project(cleaned_proj, path)
|
||||||
|
return OK
|
||||||
|
except (OSError, IOError, PermissionError, RuntimeError) as e:
|
||||||
|
return Result(data=None, errors=[ErrorInfo(
|
||||||
|
kind=ErrorKind.INTERNAL,
|
||||||
|
message=str(e),
|
||||||
|
source=f"app_controller._flush_to_project_result[{path}]",
|
||||||
|
original=e,
|
||||||
|
)])
|
||||||
|
|
||||||
|
def _deserialize_active_track_result(self, at_data: dict) -> "Result[Any]":
|
||||||
|
"""Phase 6 Group 6.7: deserialize active_track with Result propagation.
|
||||||
|
On failure: TypeError/ValueError/KeyError/AttributeError -> ErrorInfo(original=e).
|
||||||
|
Caller (`_refresh_from_project`) appends to `self._last_request_errors`."""
|
||||||
|
try:
|
||||||
|
tickets = []
|
||||||
|
for t_data in at_data.get("tickets", []):
|
||||||
|
tickets.append(models.Ticket(**t_data))
|
||||||
|
track = models.Track(
|
||||||
|
id=at_data.get("id"),
|
||||||
|
description=at_data.get("description"),
|
||||||
|
tickets=tickets
|
||||||
|
)
|
||||||
|
return Result(data=track)
|
||||||
|
except (TypeError, ValueError, KeyError, AttributeError) as e:
|
||||||
|
return Result(data=None, errors=[ErrorInfo(
|
||||||
|
kind=ErrorKind.INVALID_INPUT,
|
||||||
|
message=str(e),
|
||||||
|
source="app_controller._deserialize_active_track_result",
|
||||||
|
original=e,
|
||||||
|
)])
|
||||||
|
|
||||||
|
def _serialize_tool_calls_result(self, tool_calls: list) -> "Result[str]":
|
||||||
|
"""Phase 6 Group 6.7: json-serialize tool_calls with Result propagation.
|
||||||
|
On failure: TypeError/ValueError -> ErrorInfo(original=e).
|
||||||
|
Caller falls back to '[TOOL CALLS PRESENT]' marker."""
|
||||||
|
try:
|
||||||
|
tc_str = json.dumps(tool_calls, indent=1)
|
||||||
|
return Result(data=tc_str)
|
||||||
|
except (TypeError, ValueError) as e:
|
||||||
|
return Result(data="", errors=[ErrorInfo(
|
||||||
|
kind=ErrorKind.INVALID_INPUT,
|
||||||
|
message=str(e),
|
||||||
|
source="app_controller._serialize_tool_calls_result",
|
||||||
|
original=e,
|
||||||
|
)])
|
||||||
|
|
||||||
|
def _parse_token_history_first_ts_result(self, item: dict) -> "Result[float]":
|
||||||
|
"""Phase 6 Group 6.7: parse the first token_history timestamp.
|
||||||
|
On failure: ValueError/TypeError/KeyError/IndexError -> ErrorInfo(original=e).
|
||||||
|
Caller falls back to time.time() and records the error."""
|
||||||
|
try:
|
||||||
|
import datetime as _dt
|
||||||
|
first_ts = item['time']
|
||||||
|
dt = _dt.datetime.strptime(first_ts, '%Y-%m-%dT%H:%M:%S')
|
||||||
|
return Result(data=dt.timestamp())
|
||||||
|
except (ValueError, TypeError, KeyError, IndexError) as e:
|
||||||
|
return Result(data=0.0, errors=[ErrorInfo(
|
||||||
|
kind=ErrorKind.INVALID_INPUT,
|
||||||
|
message=str(e),
|
||||||
|
source="app_controller._parse_token_history_first_ts_result",
|
||||||
|
original=e,
|
||||||
|
)])
|
||||||
|
|
||||||
entries = []
|
entries = []
|
||||||
disc_entries = []
|
disc_entries = []
|
||||||
paired_tools = {}
|
paired_tools = {}
|
||||||
@@ -2223,14 +2323,15 @@ class AppController:
|
|||||||
tool_calls = payload.get("tool_calls", [])
|
tool_calls = payload.get("tool_calls", [])
|
||||||
content = text
|
content = text
|
||||||
if tool_calls:
|
if tool_calls:
|
||||||
try:
|
tc_result = self._serialize_tool_calls_result(tool_calls)
|
||||||
tc_str = json.dumps(tool_calls, indent=1)
|
if tc_result.ok:
|
||||||
|
tc_str = tc_result.data
|
||||||
if content:
|
if content:
|
||||||
content += f"\n\n[TOOL CALLS]\n{tc_str}"
|
content += f"\n\n[TOOL CALLS]\n{tc_str}"
|
||||||
else:
|
else:
|
||||||
content = f"[TOOL CALLS]\n{tc_str}"
|
content = f"[TOOL CALLS]\n{tc_str}"
|
||||||
except (TypeError, ValueError) as e:
|
else:
|
||||||
logging.getLogger(__name__).debug("tool_calls json serialization failed: %s", e, extra={"source": "app_controller.cb_load_prior_log.tool_calls"})
|
self._last_request_errors.append(("tool_calls_json_serialize", tc_result.errors[0]))
|
||||||
if content:
|
if content:
|
||||||
content += f"\n\n[TOOL CALLS PRESENT]"
|
content += f"\n\n[TOOL CALLS PRESENT]"
|
||||||
else:
|
else:
|
||||||
@@ -2266,14 +2367,12 @@ class AppController:
|
|||||||
self.mma_tier_usage = new_mma_usage
|
self.mma_tier_usage = new_mma_usage
|
||||||
self._token_history = new_token_history
|
self._token_history = new_token_history
|
||||||
if new_token_history:
|
if new_token_history:
|
||||||
try:
|
result = self._parse_token_history_first_ts_result(new_token_history[0])
|
||||||
import datetime
|
if result.ok:
|
||||||
first_ts = new_token_history[0]['time']
|
self._session_start_time = result.data
|
||||||
dt = datetime.datetime.strptime(first_ts, '%Y-%m-%dT%H:%M:%S')
|
else:
|
||||||
self._session_start_time = dt.timestamp()
|
|
||||||
except (ValueError, TypeError, KeyError, IndexError) as e:
|
|
||||||
logging.getLogger(__name__).debug("token_history first_ts parse failed: %s", e, extra={"source": "app_controller.cb_load_prior_log.token_history"})
|
|
||||||
self._session_start_time = time.time()
|
self._session_start_time = time.time()
|
||||||
|
self._last_request_errors.append(("token_history_first_ts_parse", result.errors[0]))
|
||||||
self.prior_session_entries = entries
|
self.prior_session_entries = entries
|
||||||
self.prior_disc_entries = disc_entries
|
self.prior_disc_entries = disc_entries
|
||||||
self.prior_tool_calls = final_tool_calls
|
self.prior_tool_calls = final_tool_calls
|
||||||
@@ -2333,14 +2432,52 @@ class AppController:
|
|||||||
|
|
||||||
self.submit_io(run_manual_prune)
|
self.submit_io(run_manual_prune)
|
||||||
|
|
||||||
|
def _load_project_from_path_result(self, pp: str) -> "Result[Dict[str, Any]]":
|
||||||
|
"""Phase 6 Group 6.7: load a project from a path with Result propagation.
|
||||||
|
On failure: OSError/IOError/ValueError/TypeError/KeyError/AttributeError/tomllib.TOMLDecodeError
|
||||||
|
-> ErrorInfo(original=e). Caller stores the error for sub-track 4 GUI."""
|
||||||
|
try:
|
||||||
|
data = project_manager.load_project(pp)
|
||||||
|
return Result(data=data)
|
||||||
|
except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, tomllib.TOMLDecodeError) as e:
|
||||||
|
return Result(data={}, errors=[ErrorInfo(
|
||||||
|
kind=ErrorKind.NOT_FOUND,
|
||||||
|
message=str(e),
|
||||||
|
source=f"app_controller._load_project_from_path_result[{pp}]",
|
||||||
|
original=e,
|
||||||
|
)])
|
||||||
|
|
||||||
|
def _save_fallback_project_result(self, fallback_path: str) -> "Result[None]":
|
||||||
|
"""Phase 6 Group 6.7: save fallback project with Result propagation.
|
||||||
|
On failure: OSError/IOError/PermissionError/RuntimeError -> ErrorInfo(original=e).
|
||||||
|
The save is best-effort; the app continues even on failure
|
||||||
|
(active_project_path stays empty; next save will use a proper path).
|
||||||
|
Per post-completion patch cb68d86f: catches RuntimeError too
|
||||||
|
(test sandbox FR1 guard raises RuntimeError)."""
|
||||||
|
try:
|
||||||
|
project_manager.save_project(self.project, fallback_path)
|
||||||
|
self.active_project_path = fallback_path
|
||||||
|
if fallback_path not in self.project_paths:
|
||||||
|
self.project_paths.append(fallback_path)
|
||||||
|
return OK
|
||||||
|
except (OSError, IOError, PermissionError, RuntimeError) as e:
|
||||||
|
return Result(data=None, errors=[ErrorInfo(
|
||||||
|
kind=ErrorKind.INTERNAL,
|
||||||
|
message=str(e),
|
||||||
|
source=f"app_controller._save_fallback_project_result[{fallback_path}]",
|
||||||
|
original=e,
|
||||||
|
)])
|
||||||
|
|
||||||
def _load_active_project(self) -> Result[None]:
|
def _load_active_project(self) -> Result[None]:
|
||||||
"""Loads the active project configuration, with fallbacks."""
|
"""Loads the active project configuration, with fallbacks."""
|
||||||
|
aggregated_errors: List[ErrorInfo] = []
|
||||||
if self.active_project_path and Path(self.active_project_path).exists():
|
if self.active_project_path and Path(self.active_project_path).exists():
|
||||||
try:
|
result = self._load_project_from_path_result(self.active_project_path)
|
||||||
self.project = project_manager.load_project(self.active_project_path)
|
if result.ok:
|
||||||
except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, tomllib.TOMLDecodeError) as e:
|
self.project = result.data
|
||||||
logging.getLogger(__name__).debug("load_project failed: %s", e, extra={"source": "app_controller._load_active_project.primary"})
|
else:
|
||||||
print(f"Failed to load project {self.active_project_path}: {e}")
|
aggregated_errors.append(result.errors[0])
|
||||||
|
print(f"Failed to load project {self.active_project_path}: {result.errors[0].message}")
|
||||||
self.project = project_manager.migrate_from_legacy_config(self.config)
|
self.project = project_manager.migrate_from_legacy_config(self.config)
|
||||||
self.active_project_path = ""
|
self.active_project_path = ""
|
||||||
else:
|
else:
|
||||||
@@ -2349,36 +2486,28 @@ class AppController:
|
|||||||
if not self.active_project_path:
|
if not self.active_project_path:
|
||||||
for pp in self.project_paths:
|
for pp in self.project_paths:
|
||||||
if Path(pp).exists():
|
if Path(pp).exists():
|
||||||
try:
|
result = self._load_project_from_path_result(pp)
|
||||||
self.project = project_manager.load_project(pp)
|
if result.ok:
|
||||||
|
self.project = result.data
|
||||||
self.active_project_path = pp
|
self.active_project_path = pp
|
||||||
break
|
break
|
||||||
except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, tomllib.TOMLDecodeError) as e:
|
aggregated_errors.append(result.errors[0])
|
||||||
logging.getLogger(__name__).debug("load_project failed for %s: %s", pp, e, extra={"source": "app_controller._load_active_project.fallback_loop"})
|
continue
|
||||||
continue
|
|
||||||
if not self.active_project_path:
|
if not self.active_project_path:
|
||||||
name = self.project.get("project", {}).get("name", "project")
|
name = self.project.get("project", {}).get("name", "project")
|
||||||
fallback_path = f"{name}.toml"
|
fallback_path = f"{name}.toml"
|
||||||
try:
|
save_result = self._save_fallback_project_result(fallback_path)
|
||||||
project_manager.save_project(self.project, fallback_path)
|
if not save_result.ok:
|
||||||
self.active_project_path = fallback_path
|
aggregated_errors.append(save_result.errors[0])
|
||||||
if fallback_path not in self.project_paths:
|
|
||||||
self.project_paths.append(fallback_path)
|
|
||||||
except (OSError, IOError, PermissionError, RuntimeError) as e:
|
|
||||||
logging.getLogger(__name__).debug(
|
|
||||||
"Could not save fallback project to %s: %s", fallback_path, e,
|
|
||||||
extra={"source": "app_controller._load_active_project.fallback_save"},
|
|
||||||
)
|
|
||||||
# The save is best-effort; the app can still operate without persisting
|
|
||||||
# the empty fallback (e.g., when the test sandbox FR1 guard blocks
|
|
||||||
# writes to the project root via the sys.addaudithook RuntimeError).
|
|
||||||
# active_project_path stays empty; the next save will use a proper path.
|
|
||||||
self.preset_manager = presets.PresetManager(Path(self.active_project_path).parent if self.active_project_path else None)
|
self.preset_manager = presets.PresetManager(Path(self.active_project_path).parent if self.active_project_path else None)
|
||||||
self.tool_preset_manager = tool_presets.ToolPresetManager(Path(self.active_project_path).parent if self.active_project_path else None)
|
self.tool_preset_manager = tool_presets.ToolPresetManager(Path(self.active_project_path).parent if self.active_project_path else None)
|
||||||
from src.personas import PersonaManager
|
from src.personas import PersonaManager
|
||||||
self.persona_manager = PersonaManager(Path(self.active_project_path).parent if self.active_project_path else None)
|
self.persona_manager = PersonaManager(Path(self.active_project_path).parent if self.active_project_path else None)
|
||||||
self._refresh_from_project()
|
self._refresh_from_project()
|
||||||
self._configure_mcp_for_project()
|
self._configure_mcp_for_project()
|
||||||
|
if aggregated_errors:
|
||||||
|
return Result(data=None, errors=aggregated_errors)
|
||||||
|
return OK
|
||||||
|
|
||||||
def inject_context(self, data: dict) -> None:
|
def inject_context(self, data: dict) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -2600,17 +2729,32 @@ class AppController:
|
|||||||
def _run_event_loop(self):
|
def _run_event_loop(self):
|
||||||
"""Internal loop runner."""
|
"""Internal loop runner."""
|
||||||
|
|
||||||
def queue_fallback() -> None:
|
def queue_fallback() -> "Result[None]":
|
||||||
while True:
|
while True:
|
||||||
try:
|
result = self._run_pending_tasks_once_result()
|
||||||
if hasattr(self, '_process_pending_gui_tasks'):
|
if not result.ok:
|
||||||
self._process_pending_gui_tasks()
|
self._report_worker_error("queue_fallback", result)
|
||||||
if hasattr(self, '_process_pending_history_adds'):
|
|
||||||
self._process_pending_history_adds()
|
|
||||||
except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, RuntimeError) as e:
|
|
||||||
logging.getLogger(__name__).debug("queue fallback task error: %s", e, extra={"source": "app_controller.queue_fallback"})
|
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
self.submit_io(queue_fallback)
|
self.submit_io(queue_fallback)
|
||||||
|
|
||||||
|
def _run_pending_tasks_once_result(self) -> "Result[None]":
|
||||||
|
"""Phase 6 Group 6.7 (bounded retry Pattern 5 drain): single iteration of
|
||||||
|
the queue-fallback loop. On failure: any handler exception -> ErrorInfo.
|
||||||
|
Caller (`queue_fallback`) reports via _report_worker_error; the bounded
|
||||||
|
retry loop IS the Pattern 5 drain."""
|
||||||
|
try:
|
||||||
|
if hasattr(self, '_process_pending_gui_tasks'):
|
||||||
|
self._process_pending_gui_tasks()
|
||||||
|
if hasattr(self, '_process_pending_history_adds'):
|
||||||
|
self._process_pending_history_adds()
|
||||||
|
return OK
|
||||||
|
except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, RuntimeError) as e:
|
||||||
|
return Result(data=None, errors=[ErrorInfo(
|
||||||
|
kind=ErrorKind.INTERNAL,
|
||||||
|
message=str(e),
|
||||||
|
source="app_controller._run_pending_tasks_once_result",
|
||||||
|
original=e,
|
||||||
|
)])
|
||||||
self._process_event_queue()
|
self._process_event_queue()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -2824,13 +2968,9 @@ class AppController:
|
|||||||
|
|
||||||
cleaned_proj = project_manager.clean_nones(proj)
|
cleaned_proj = project_manager.clean_nones(proj)
|
||||||
if self.active_project_path:
|
if self.active_project_path:
|
||||||
try:
|
result = self._flush_to_project_result(cleaned_proj, self.active_project_path)
|
||||||
project_manager.save_project(cleaned_proj, self.active_project_path)
|
if not result.ok:
|
||||||
except (OSError, IOError, PermissionError, RuntimeError) as e:
|
self._last_request_errors.append(("flush_to_project", result.errors[0]))
|
||||||
logging.getLogger(__name__).debug(
|
|
||||||
"Could not save project to %s: %s", self.active_project_path, e,
|
|
||||||
extra={"source": "app_controller._flush_to_project"},
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
logging.getLogger(__name__).debug(
|
logging.getLogger(__name__).debug(
|
||||||
"Skipping _flush_to_project: active_project_path is empty.",
|
"Skipping _flush_to_project: active_project_path is empty.",
|
||||||
@@ -3124,19 +3264,14 @@ class AppController:
|
|||||||
self.mma_tier_usage[tier]["tool_preset"] = data.get("tool_preset", self.mma_tier_usage[tier].get("tool_preset"))
|
self.mma_tier_usage[tier]["tool_preset"] = data.get("tool_preset", self.mma_tier_usage[tier].get("tool_preset"))
|
||||||
at_data = mma_sec.get("active_track")
|
at_data = mma_sec.get("active_track")
|
||||||
if at_data:
|
if at_data:
|
||||||
try:
|
result = self._deserialize_active_track_result(at_data)
|
||||||
tickets = []
|
if result.ok:
|
||||||
for t_data in at_data.get("tickets", []):
|
self.active_track = result.data
|
||||||
tickets.append(models.Ticket(**t_data))
|
|
||||||
self.active_track = models.Track(
|
|
||||||
id=at_data.get("id"),
|
|
||||||
description=at_data.get("description"),
|
|
||||||
tickets=tickets
|
|
||||||
)
|
|
||||||
self.active_tickets = at_data.get("tickets", []) # Keep dicts for UI table
|
self.active_tickets = at_data.get("tickets", []) # Keep dicts for UI table
|
||||||
except (TypeError, ValueError, KeyError, AttributeError) as e:
|
else:
|
||||||
logging.getLogger(__name__).debug("active track deserialize failed: %s", e, extra={"source": "app_controller._refresh_from_project.active_track"})
|
err = result.errors[0]
|
||||||
print(f"Failed to deserialize active track: {e}")
|
self._last_request_errors.append(("active_track_deserialize", err))
|
||||||
|
print(f"Failed to deserialize active track: {err.message}")
|
||||||
self.active_track = None
|
self.active_track = None
|
||||||
else:
|
else:
|
||||||
self.active_track = None
|
self.active_track = None
|
||||||
|
|||||||
Reference in New Issue
Block a user