Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 142 additions & 19 deletions app/living_ui/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,12 @@ async def _watchdog_loop(self) -> None:
f"[LIVING_UI:WATCHDOG] {project.name} ({project_id}) restarted successfully"
)
retry_counts.pop(project_id, None)
self._save_projects()
try:
self._save_projects()
except Exception:
logger.exception(
"[LIVING_UI:WATCHDOG] Restart succeeded but state could not be persisted"
)

except asyncio.CancelledError:
break
Expand Down Expand Up @@ -439,7 +444,12 @@ async def _escalate_crash(self, project_id: str, crash_targets: List[str]) -> No
project.error = f"{crash_str} crashed after {len(self.WATCHDOG_RETRY_DELAYS)} restart attempts"
project.process = None
project.backend_process = None
self._save_projects()
try:
self._save_projects()
except Exception:
logger.exception(
"[LIVING_UI:WATCHDOG] Failed to persist crash state during escalation"
)

# Create agent task to investigate and fix
if not self._task_manager or not self._trigger_queue:
Expand Down Expand Up @@ -511,7 +521,13 @@ async def _escalate_crash(self, project_id: str, crash_targets: List[str]) -> No
await self._trigger_queue.put(trigger)

project.task_id = task_id
self._save_projects()
try:
self._save_projects()
except Exception:
logger.exception(
"[LIVING_UI:WATCHDOG] Failed to persist crash task association"
)

logger.info(
f"[LIVING_UI:WATCHDOG] Created fix task {task_id} for {project.name} ({project_id})"
)
Expand Down Expand Up @@ -585,6 +601,7 @@ def _save_projects(self) -> None:
json.dump(data, f, indent=2)
except Exception as e:
logger.error(f"[LIVING_UI] Failed to save projects: {e}")
raise

def _allocate_port(self) -> int:
"""Allocate a free port for a Living UI project.
Expand Down Expand Up @@ -1053,7 +1070,13 @@ async def launch_and_verify(self, project_id: str) -> dict:
logger.error(f"[LIVING_UI:PIPELINE] {err}")
project.status = "error"
project.error = f"{len(all_errors)} validation error(s)"
self._save_projects()
try:
self._save_projects()
except Exception:
logger.exception(
"[LIVING_UI] Failed to persist validation error state for project %s",
project.id,
)
return {"status": "error", "step": "validation", "errors": all_errors}

logger.info("[LIVING_UI:PIPELINE] All validation passed, starting servers...")
Expand Down Expand Up @@ -1205,7 +1228,19 @@ async def launch_and_verify(self, project_id: str) -> dict:
# === SUCCESS ===
project.status = "running"
project.error = None
self._save_projects()
try:
self._save_projects()
except Exception:
logger.error(
"[LIVING_UI] Project running but state not persisted — restart will lose it"
)
return {
"status": "success",
"url": project.url,
"backend_url": project.backend_url,
"port": project.port,
"warning": "state_not_persisted",
}
self._save_launch_timestamp(project_path)

logger.info(
Expand Down Expand Up @@ -1344,7 +1379,19 @@ async def _launch_servers_only(

project.status = "running"
project.error = None
self._save_projects()
try:
self._save_projects()
except Exception:
logger.error(
"[LIVING_UI] Project running but state not persisted — restart will lose it"
)
return {
"status": "success",
"url": project.url,
"backend_url": project.backend_url,
"port": project.port,
"warning": "state_not_persisted",
}
self._save_launch_timestamp(project_path)

logger.info(
Expand Down Expand Up @@ -2164,7 +2211,10 @@ def cleanup_on_startup(self) -> None:
project.backend_process = None
project.url = None
project.backend_url = None
self._save_projects()
try:
self._save_projects()
except Exception:
logger.error("[LIVING_UI] Failed to persist startup cleanup state")

logger.info("[LIVING_UI] Startup cleanup complete")

Expand Down Expand Up @@ -2271,8 +2321,13 @@ async def create_project(
)

self.projects[project_id] = project
self._save_projects()

try:
self._save_projects()
except Exception:
logger.exception(
"[LIVING_UI] Failed to persist newly created project %s",
project_id,
)
logger.info(f"[LIVING_UI] Created project: {name} ({project_id})")
return project

Expand Down Expand Up @@ -2464,7 +2519,13 @@ async def install_from_marketplace(
)

self.projects[project_id] = project
self._save_projects()
try:
self._save_projects()
except Exception:
logger.exception(
"[LIVING_UI:MARKETPLACE] Failed to persist project %s",
project_id,
)

logger.info(
f"[LIVING_UI:MARKETPLACE] Created project: {app_name} ({project_id})"
Expand Down Expand Up @@ -2511,7 +2572,14 @@ def update_project_status(
self.projects[project_id].status = status
if error:
self.projects[project_id].error = error
self._save_projects()

try:
self._save_projects()
except Exception:
logger.exception(
"[LIVING_UI] Failed to persist status update for project %s",
project_id,
)

def set_project_task(self, project_id: str, task_id: str) -> None:
"""Associate a task ID with a project."""
Expand Down Expand Up @@ -2845,7 +2913,18 @@ async def _launch_single_process(

project.backend_url = f"http://localhost:{app_port}"
project.status = "running"
self._save_projects()
try:
self._save_projects()
except Exception:
logger.exception(
"[LIVING_UI] Project running but state not persisted — restart will lose it"
)
return {
"status": "success",
"url": project.url,
"port": proxy_port,
"warning": "state_not_persisted",
}

logger.info(f"[LIVING_UI:PIPELINE] App ready: {project.url}")
return {
Expand Down Expand Up @@ -3037,7 +3116,13 @@ async def import_external_app(
if existing and existing.task_id:
project.task_id = existing.task_id
self.projects[project_id] = project
self._save_projects()
try:
self._save_projects()
except Exception:
logger.exception(
"[LIVING_UI] Failed to persist imported external app %s",
project_id,
)

logger.info(f"[LIVING_UI] Imported external app: {name} ({project_id})")
return {
Expand Down Expand Up @@ -3136,7 +3221,13 @@ async def stop_project(self, project_id: str, stop_backend: bool = True) -> bool
await self.stop_backend(project_id)

project.status = "stopped"
self._save_projects()
try:
self._save_projects()
except Exception:
logger.exception(
"[LIVING_UI] Failed to persist stopped state for project %s",
project_id,
)

logger.info(f"[LIVING_UI] Stopped project: {project_id}")
return True
Expand Down Expand Up @@ -3179,7 +3270,14 @@ async def delete_project(self, project_id: str) -> bool:

# Remove from registry
del self.projects[project_id]
self._save_projects()

try:
self._save_projects()
except Exception:
logger.exception(
"[LIVING_UI] Failed to persist deletion of project %s",
project_id,
)

logger.info(f"[LIVING_UI] Deleted project: {project_id}")
return True
Expand Down Expand Up @@ -3358,7 +3456,13 @@ async def import_project_zip(
if existing and existing.task_id:
project.task_id = existing.task_id
self.projects[project_id] = project
self._save_projects()
try:
self._save_projects()
except Exception:
logger.exception(
"[LIVING_UI] Failed to persist imported project %s",
project_id,
)

logger.info(f"[LIVING_UI] Imported project '{name}' ({project_id}) from ZIP")
return project
Expand Down Expand Up @@ -3548,7 +3652,13 @@ async def start_tunnel(
if url:
project.tunnel_process = proc
project.tunnel_url = url
self._save_projects()
try:
self._save_projects()
except Exception:
logger.exception(
"[LIVING_UI] Failed to persist tunnel state for project %s",
project_id,
)
logger.info(f"[LIVING_UI] Tunnel started for {project.name}: {url}")
return url
else:
Expand All @@ -3565,7 +3675,14 @@ async def stop_tunnel(self, project_id: str) -> None:
self._terminate_process(project.tunnel_process)
project.tunnel_process = None
project.tunnel_url = None
self._save_projects()
try:
self._save_projects()
except Exception:
logger.exception(
"[LIVING_UI] Failed to persist stopped tunnel state for project %s",
project_id,
)

logger.info(f"[LIVING_UI] Tunnel stopped for {project.name}")

async def _parse_cloudflare_url(
Expand Down Expand Up @@ -3626,5 +3743,11 @@ async def auto_launch_projects(self, project_ids: List[str] = None) -> None:
f"[LIVING_UI] Auto-launching: {project.name} ({project_id})"
)
project.status = "launching"
self._save_projects()
try:
self._save_projects()
except Exception:
logger.exception(
"[LIVING_UI] Failed to persist launching state for project %s",
project_id,
)
await self.launch_project(project_id)
14 changes: 12 additions & 2 deletions app/ui_layer/settings/profile_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,12 @@ def _register_living_ui_via_manager(
manager._used_ports.add(project_obj.port)
if project_obj.backend_port:
manager._used_ports.add(project_obj.backend_port)
manager._save_projects()
try:
manager._save_projects()
except Exception:
logger.exception(
"[PROFILE_BUNDLE] Failed to persist imported Living UI projects"
)


def _register_living_ui_via_file(records: List[Dict[str, Any]]) -> None:
Expand Down Expand Up @@ -971,7 +976,12 @@ def _wipe_living_ui_state(
if manager is not None:
manager.projects.clear()
manager._used_ports.clear()
manager._save_projects()
try:
manager._save_projects()
except Exception:
logger.exception(
"[PROFILE_BUNDLE] Failed to persist cleared Living UI state"
)
else:
LIVING_UI_PROJECTS_FILE.parent.mkdir(parents=True, exist_ok=True)
LIVING_UI_PROJECTS_FILE.write_text(
Expand Down