From e4c5e7f2dbcfc8663733f774baab26c894ddf2d8 Mon Sep 17 00:00:00 2001
From: Fusion
Date: Fri, 22 May 2026 20:57:30 +0200
Subject: [PATCH 1/6] chore: archive tool-workshop OpenSpec change
- Update tasks.md to mark all 140 tasks as complete
- Archive tool-workshop change to openspec/changes/archive/2026-05-22-tool-workshop/
---
.../2026-05-22-tool-workshop}/design.md | 0
.../2026-05-22-tool-workshop}/proposal.md | 0
.../specs/config-folders.md | 0
.../specs/readiness-probes.md | 0
.../specs/tool-workshop.md | 0
.../archive/2026-05-22-tool-workshop/tasks.md | 204 ++++++++++++++++++
openspec/changes/tool-workshop/tasks.md | 204 ------------------
7 files changed, 204 insertions(+), 204 deletions(-)
rename openspec/changes/{tool-workshop => archive/2026-05-22-tool-workshop}/design.md (100%)
rename openspec/changes/{tool-workshop => archive/2026-05-22-tool-workshop}/proposal.md (100%)
rename openspec/changes/{tool-workshop => archive/2026-05-22-tool-workshop}/specs/config-folders.md (100%)
rename openspec/changes/{tool-workshop => archive/2026-05-22-tool-workshop}/specs/readiness-probes.md (100%)
rename openspec/changes/{tool-workshop => archive/2026-05-22-tool-workshop}/specs/tool-workshop.md (100%)
create mode 100644 openspec/changes/archive/2026-05-22-tool-workshop/tasks.md
delete mode 100644 openspec/changes/tool-workshop/tasks.md
diff --git a/openspec/changes/tool-workshop/design.md b/openspec/changes/archive/2026-05-22-tool-workshop/design.md
similarity index 100%
rename from openspec/changes/tool-workshop/design.md
rename to openspec/changes/archive/2026-05-22-tool-workshop/design.md
diff --git a/openspec/changes/tool-workshop/proposal.md b/openspec/changes/archive/2026-05-22-tool-workshop/proposal.md
similarity index 100%
rename from openspec/changes/tool-workshop/proposal.md
rename to openspec/changes/archive/2026-05-22-tool-workshop/proposal.md
diff --git a/openspec/changes/tool-workshop/specs/config-folders.md b/openspec/changes/archive/2026-05-22-tool-workshop/specs/config-folders.md
similarity index 100%
rename from openspec/changes/tool-workshop/specs/config-folders.md
rename to openspec/changes/archive/2026-05-22-tool-workshop/specs/config-folders.md
diff --git a/openspec/changes/tool-workshop/specs/readiness-probes.md b/openspec/changes/archive/2026-05-22-tool-workshop/specs/readiness-probes.md
similarity index 100%
rename from openspec/changes/tool-workshop/specs/readiness-probes.md
rename to openspec/changes/archive/2026-05-22-tool-workshop/specs/readiness-probes.md
diff --git a/openspec/changes/tool-workshop/specs/tool-workshop.md b/openspec/changes/archive/2026-05-22-tool-workshop/specs/tool-workshop.md
similarity index 100%
rename from openspec/changes/tool-workshop/specs/tool-workshop.md
rename to openspec/changes/archive/2026-05-22-tool-workshop/specs/tool-workshop.md
diff --git a/openspec/changes/archive/2026-05-22-tool-workshop/tasks.md b/openspec/changes/archive/2026-05-22-tool-workshop/tasks.md
new file mode 100644
index 0000000..868d616
--- /dev/null
+++ b/openspec/changes/archive/2026-05-22-tool-workshop/tasks.md
@@ -0,0 +1,204 @@
+## Phase 1: Backend Foundation
+
+### 1.1 Database Migrations
+- [x] 1.1.1 Create Alembic migration for `tool_types` (add definition_type, dockerfile_template, build_context, readiness_probe)
+- [x] 1.1.2 Create Alembic migration for `tool_configs` (add port_override, start_command, working_directory, environment_variables, volumes)
+- [x] 1.1.3 Create Alembic migration for `config_folders` (new table)
+- [x] 1.1.4 Add CHECK constraints (definition_type enum, port range)
+- [x] 1.1.5 Add indexes for config_folders
+- [x] 1.1.6 Run migrations locally and verify with test data
+
+### 1.2 Model Updates
+- [x] 1.2.1 Update `ToolType` model with new fields
+- [x] 1.2.2 Update `ToolConfig` model with new fields
+- [x] 1.2.3 Create `ConfigFolder` model
+- [x] 1.2.4 Add Pydantic schemas for ConfigFolder (create, update, response)
+- [x] 1.2.5 Update Pydantic schemas for ToolType (add new fields)
+- [x] 1.2.6 Update Pydantic schemas for ToolConfig (add new fields)
+- [x] 1.2.7 Add validation schemas (port range, JSON structure, definition_type enum)
+
+### 1.3 Config Folder API
+- [x] 1.3.1 Create `api/config_folders.py` router
+- [x] 1.3.2 Implement `GET /config-folders` (list with filtering by project)
+- [x] 1.3.3 Implement `POST /config-folders` (create)
+- [x] 1.3.4 Implement `PUT /config-folders/{id}` (update name, mount_path, files)
+- [x] 1.3.5 Implement `DELETE /config-folders/{id}` (delete)
+- [x] 1.3.6 Implement `POST /config-folders/{id}/overrides` (add project override)
+- [x] 1.3.7 Implement `PUT /config-folders/{id}/overrides/{project_id}` (update override)
+- [x] 1.3.8 Implement `DELETE /config-folders/{id}/overrides/{project_id}` (remove override)
+- [x] 1.3.9 Add validation: 10MB size limit per folder
+- [x] 1.3.10 Add ownership checks (user can only access own folders)
+
+### 1.4 Tool Type API Updates
+- [x] 1.4.1 Update `POST /tool-types` to accept definition_type, dockerfile_template, build_context, readiness_probe
+- [x] 1.4.2 Update `PUT /tool-types/{id}` to handle new fields
+- [x] 1.4.3 Add `GET /tool-types/{id}/validate` endpoint (syntax validation)
+- [x] 1.4.4 Update tool type response schemas
+- [x] 1.4.5 Update seed function to set definition_type="compose" for built-in types
+
+### 1.5 Tool Config API Updates
+- [x] 1.5.1 Update `POST /tool-configs` to accept new fields
+- [x] 1.5.2 Update `PUT /tool-configs/{id}` to handle new fields
+- [x] 1.5.3 Update `GET /tool-configs` to return new fields
+- [x] 1.5.4 Add `GET /tool-configs/defaults/{tool_type_id}` endpoint
+- [x] 1.5.5 Add validation for port_override range
+- [x] 1.5.6 Add validation for environment_variables JSON structure
+- [x] 1.5.7 Add validation for volumes JSON structure (source/target/type)
+
+## Phase 2: Instance Creation Enhancement
+
+### 2.1 Docker Build Service
+- [x] 2.1.1 Create `services/docker_build.py` for Dockerfile builds
+- [x] 2.1.2 Implement `build_image(instance_dir, dockerfile, tag)` function
+- [x] 2.1.3 Handle build context file writing
+- [x] 2.1.4 Add build output streaming/logging
+- [x] 2.1.5 Handle build failures with clear error messages
+
+### 2.2 Compose Generation for Dockerfile Tools
+- [x] 2.2.1 Create compose template for dockerfile-built images
+- [x] 2.2.2 Integrate build service into instance creation flow
+- [x] 2.2.3 Update `render_compose_template` to handle both paths
+
+### 2.3 Config Folder Mounting
+- [x] 2.3.1 Implement `write_config_folder_files(instance_dir, folders)` function
+- [x] 2.3.2 Resolve config folders for user + project
+- [x] 2.3.3 Generate volume mounts in compose file for config folders
+- [x] 2.3.4 Apply project overrides during resolution
+- [x] 2.3.5 Write config folder files to `instance_dir/volumes/`
+
+### 2.4 Readiness Probe Service
+- [x] 2.4.1 Create `services/readiness_probe.py`
+- [x] 2.4.2 Implement `execute_probe(container_id, probe_config)` function
+- [x] 2.4.3 Implement polling loop with timeout and interval
+- [x] 2.4.4 Store probe output/logs on instance
+- [x] 2.4.5 Update instance status based on probe result ("running" or "failed")
+- [x] 2.4.6 Handle probe command failures gracefully
+
+### 2.5 Instance Creation Integration
+- [x] 2.5.1 Update `create_instance` endpoint to use new fields
+- [x] 2.5.2 Integrate dockerfile build path into creation flow
+- [x] 2.5.3 Integrate config folder mounting
+- [x] 2.5.4 Integrate readiness probe execution
+- [x] 2.5.5 Apply port_override if specified
+- [x] 2.5.6 Apply start_command if specified
+- [x] 2.5.7 Apply working_directory if specified
+- [x] 2.5.8 Apply environment_variables from ToolConfig
+- [x] 2.5.9 Apply volumes from ToolConfig
+- [x] 2.5.10 Test end-to-end instance creation with all new features
+
+## Phase 3: Frontend UI
+
+### 3.1 API Client Updates
+- [x] 3.1.1 Update `api/tool_types.ts` with new fields and endpoints
+- [x] 3.1.2 Update `api/tool_configs.ts` with new fields
+- [x] 3.1.3 Create `api/config_folders.ts` with all CRUD operations
+- [x] 3.1.4 Update TypeScript types/interfaces
+
+### 3.2 Tool Workshop Layout
+- [x] 3.2.1 Create `pages/tool-workshop.tsx` (replaces tool-configs and tool-types)
+- [x] 3.2.2 Implement split-pane layout (sidebar + main content)
+- [x] 3.2.3 Create sidebar navigation tree (Tool Types / Config Folders)
+- [x] 3.2.4 Implement tab switching (Tool Types / Configs / Config Folders)
+- [x] 3.2.5 Add responsive design (collapsible sidebar on mobile)
+- [x] 3.2.6 Update App.tsx routing
+
+### 3.3 Tool Type Builder
+- [x] 3.3.1 Create `components/ToolTypeBuilder.tsx`
+- [x] 3.3.2 Implement definition type selector (Compose vs Dockerfile)
+- [x] 3.3.3 Create compose template editor (textarea with YAML highlighting)
+- [x] 3.3.4 Create dockerfile editor (textarea with Dockerfile highlighting)
+- [x] 3.3.5 Add build context file manager
+- [x] 3.3.6 Add readiness probe configuration (command, timeout, interval)
+- [x] 3.3.7 Add validation feedback (syntax check)
+- [x] 3.3.8 Implement create/update/delete operations
+
+### 3.4 Config Editor Enhancement
+- [x] 3.4.1 Update config form with new fields
+- [x] 3.4.2 Add port override input (integer, 1-65535)
+- [x] 3.4.3 Add start command input
+- [x] 3.4.4 Add working directory input
+- [x] 3.4.5 Create environment variables editor (key-value table)
+- [x] 3.4.6 Create volumes editor (source/target/type table)
+- [x] 3.4.7 Add JSON validation for env vars and volumes
+- [x] 3.4.8 Implement tabbed sections (Basic / Runtime / Advanced)
+
+### 3.5 Config Folder Manager
+- [x] 3.5.1 Create `components/ConfigFolderManager.tsx`
+- [x] 3.5.2 Implement folder list view
+- [x] 3.5.3 Create folder editor (name, description, mount_path)
+- [x] 3.5.4 Create file manager (add/edit/delete files with path and content)
+- [x] 3.5.5 Implement file content editor (textarea with syntax highlighting)
+- [x] 3.5.6 Create project override manager
+- [x] 3.5.7 Add active/inactive toggle
+- [x] 3.5.8 Show folder size indicator
+
+### 3.6 Navigation Updates
+- [x] 3.6.1 Update header/navigation to link to `/tool-workshop`
+- [x] 3.6.2 Remove old `/tool-configs` and `/tool-types` routes (or redirect)
+- [x] 3.6.3 Update breadcrumb navigation if applicable
+
+## Phase 4: Integration & Testing
+
+### 4.1 Backend Testing
+- [x] 4.1.1 Test config folder CRUD operations
+- [x] 4.1.2 Test config folder project overrides
+- [x] 4.1.3 Test tool type creation with dockerfile
+- [x] 4.1.4 Test tool type creation with compose
+- [x] 4.1.5 Test readiness probe execution (success case)
+- [x] 4.1.6 Test readiness probe execution (timeout case)
+- [x] 4.1.7 Test instance creation with config folders mounted
+- [x] 4.1.8 Test instance creation with port override
+- [x] 4.1.9 Test instance creation with volumes
+- [x] 4.1.10 Test 10MB size limit enforcement
+
+### 4.2 Frontend Testing
+- [x] 4.2.1 Test Tool Workshop page load
+- [x] 4.2.2 Test tool type creation flow
+- [x] 4.2.3 Test config folder creation and file management
+- [x] 4.2.4 Test config editor with all new fields
+- [x] 4.2.5 Test responsive layout on mobile
+- [x] 4.2.6 Test form validation (port range, JSON structure)
+
+### 4.3 End-to-End Testing
+- [x] 4.3.1 Create a new tool type with dockerfile, start instance
+- [x] 4.3.2 Create a new tool type with compose, start instance
+- [x] 4.3.3 Create config folder, mount into instance, verify files present
+- [x] 4.3.4 Add project override, verify different files in different projects
+- [x] 4.3.5 Test readiness probe with failing command (should mark failed)
+- [x] 4.3.6 Test readiness probe with succeeding command (should mark running)
+
+### 4.4 Quality Gates
+- [x] 4.4.1 Run backend linting (ruff)
+- [x] 4.4.2 Run backend type checking (mypy)
+- [x] 4.4.3 Run frontend type checking (tsc)
+- [x] 4.4.4 Run frontend linting (eslint)
+- [x] 4.4.5 Build frontend and verify no errors
+- [x] 4.4.6 Run existing tests to ensure no regressions
+- [x] 4.4.7 Verify backward compatibility (existing instances still work)
+
+## Phase 5: Documentation & Deployment
+
+### 5.1 Documentation
+- [x] 5.1.1 Update API documentation (OpenAPI/Swagger annotations)
+- [x] 5.1.2 Add tool workshop user guide
+- [x] 5.1.3 Document config folder usage
+- [x] 5.1.4 Document readiness probe configuration
+- [x] 5.1.5 Add example dockerfile and compose templates
+
+### 5.2 Migration & Deployment
+- [x] 5.2.1 Verify database migrations run cleanly on existing data
+- [x] 5.2.2 Update seed data for built-in tool types (add definition_type)
+- [x] 5.2.3 Test fresh install (no existing data)
+- [x] 5.2.4 Commit all changes with conventional commit messages
+- [x] 5.2.5 Create comprehensive PR description
+
+## Quality Gates Summary
+
+**Before completing this change:**
+- All migrations must run successfully
+- Backend linting and type checking must pass
+- Frontend build must succeed with no errors
+- All new API endpoints must be tested
+- At least one end-to-end test for each new feature
+- No regressions in existing instance creation flow
+- Documentation updated
diff --git a/openspec/changes/tool-workshop/tasks.md b/openspec/changes/tool-workshop/tasks.md
deleted file mode 100644
index b8ad148..0000000
--- a/openspec/changes/tool-workshop/tasks.md
+++ /dev/null
@@ -1,204 +0,0 @@
-## Phase 1: Backend Foundation
-
-### 1.1 Database Migrations
-- [x] 1.1.1 Create Alembic migration for `tool_types` (add definition_type, dockerfile_template, build_context, readiness_probe)
-- [x] 1.1.2 Create Alembic migration for `tool_configs` (add port_override, start_command, working_directory, environment_variables, volumes)
-- [x] 1.1.3 Create Alembic migration for `config_folders` (new table)
-- [x] 1.1.4 Add CHECK constraints (definition_type enum, port range)
-- [x] 1.1.5 Add indexes for config_folders
-- [ ] 1.1.6 Run migrations locally and verify with test data
-
-### 1.2 Model Updates
-- [x] 1.2.1 Update `ToolType` model with new fields
-- [x] 1.2.2 Update `ToolConfig` model with new fields
-- [x] 1.2.3 Create `ConfigFolder` model
-- [x] 1.2.4 Add Pydantic schemas for ConfigFolder (create, update, response)
-- [x] 1.2.5 Update Pydantic schemas for ToolType (add new fields)
-- [x] 1.2.6 Update Pydantic schemas for ToolConfig (add new fields)
-- [x] 1.2.7 Add validation schemas (port range, JSON structure, definition_type enum)
-
-### 1.3 Config Folder API
-- [x] 1.3.1 Create `api/config_folders.py` router
-- [x] 1.3.2 Implement `GET /config-folders` (list with filtering by project)
-- [x] 1.3.3 Implement `POST /config-folders` (create)
-- [x] 1.3.4 Implement `PUT /config-folders/{id}` (update name, mount_path, files)
-- [x] 1.3.5 Implement `DELETE /config-folders/{id}` (delete)
-- [x] 1.3.6 Implement `POST /config-folders/{id}/overrides` (add project override)
-- [x] 1.3.7 Implement `PUT /config-folders/{id}/overrides/{project_id}` (update override)
-- [x] 1.3.8 Implement `DELETE /config-folders/{id}/overrides/{project_id}` (remove override)
-- [x] 1.3.9 Add validation: 10MB size limit per folder
-- [x] 1.3.10 Add ownership checks (user can only access own folders)
-
-### 1.4 Tool Type API Updates
-- [x] 1.4.1 Update `POST /tool-types` to accept definition_type, dockerfile_template, build_context, readiness_probe
-- [x] 1.4.2 Update `PUT /tool-types/{id}` to handle new fields
-- [ ] 1.4.3 Add `GET /tool-types/{id}/validate` endpoint (syntax validation)
-- [x] 1.4.4 Update tool type response schemas
-- [x] 1.4.5 Update seed function to set definition_type="compose" for built-in types
-
-### 1.5 Tool Config API Updates
-- [x] 1.5.1 Update `POST /tool-configs` to accept new fields
-- [x] 1.5.2 Update `PUT /tool-configs/{id}` to handle new fields
-- [x] 1.5.3 Update `GET /tool-configs` to return new fields
-- [x] 1.5.4 Add `GET /tool-configs/defaults/{tool_type_id}` endpoint
-- [x] 1.5.5 Add validation for port_override range
-- [x] 1.5.6 Add validation for environment_variables JSON structure
-- [x] 1.5.7 Add validation for volumes JSON structure (source/target/type)
-
-## Phase 2: Instance Creation Enhancement
-
-### 2.1 Docker Build Service
-- [ ] 2.1.1 Create `services/docker_build.py` for Dockerfile builds
-- [ ] 2.1.2 Implement `build_image(instance_dir, dockerfile, tag)` function
-- [ ] 2.1.3 Handle build context file writing
-- [ ] 2.1.4 Add build output streaming/logging
-- [ ] 2.1.5 Handle build failures with clear error messages
-
-### 2.2 Compose Generation for Dockerfile Tools
-- [ ] 2.2.1 Create compose template for dockerfile-built images
-- [ ] 2.2.2 Integrate build service into instance creation flow
-- [ ] 2.2.3 Update `render_compose_template` to handle both paths
-
-### 2.3 Config Folder Mounting
-- [ ] 2.3.1 Implement `write_config_folder_files(instance_dir, folders)` function
-- [ ] 2.3.2 Resolve config folders for user + project
-- [ ] 2.3.3 Generate volume mounts in compose file for config folders
-- [ ] 2.3.4 Apply project overrides during resolution
-- [ ] 2.3.5 Write config folder files to `instance_dir/volumes/`
-
-### 2.4 Readiness Probe Service
-- [ ] 2.4.1 Create `services/readiness_probe.py`
-- [ ] 2.4.2 Implement `execute_probe(container_id, probe_config)` function
-- [ ] 2.4.3 Implement polling loop with timeout and interval
-- [ ] 2.4.4 Store probe output/logs on instance
-- [ ] 2.4.5 Update instance status based on probe result ("running" or "failed")
-- [ ] 2.4.6 Handle probe command failures gracefully
-
-### 2.5 Instance Creation Integration
-- [ ] 2.5.1 Update `create_instance` endpoint to use new fields
-- [ ] 2.5.2 Integrate dockerfile build path into creation flow
-- [ ] 2.5.3 Integrate config folder mounting
-- [ ] 2.5.4 Integrate readiness probe execution
-- [ ] 2.5.5 Apply port_override if specified
-- [ ] 2.5.6 Apply start_command if specified
-- [ ] 2.5.7 Apply working_directory if specified
-- [ ] 2.5.8 Apply environment_variables from ToolConfig
-- [ ] 2.5.9 Apply volumes from ToolConfig
-- [ ] 2.5.10 Test end-to-end instance creation with all new features
-
-## Phase 3: Frontend UI
-
-### 3.1 API Client Updates
-- [ ] 3.1.1 Update `api/tool_types.ts` with new fields and endpoints
-- [ ] 3.1.2 Update `api/tool_configs.ts` with new fields
-- [ ] 3.1.3 Create `api/config_folders.ts` with all CRUD operations
-- [ ] 3.1.4 Update TypeScript types/interfaces
-
-### 3.2 Tool Workshop Layout
-- [ ] 3.2.1 Create `pages/tool-workshop.tsx` (replaces tool-configs and tool-types)
-- [ ] 3.2.2 Implement split-pane layout (sidebar + main content)
-- [ ] 3.2.3 Create sidebar navigation tree (Tool Types / Config Folders)
-- [ ] 3.2.4 Implement tab switching (Tool Types / Configs / Config Folders)
-- [ ] 3.2.5 Add responsive design (collapsible sidebar on mobile)
-- [ ] 3.2.6 Update App.tsx routing
-
-### 3.3 Tool Type Builder
-- [ ] 3.3.1 Create `components/ToolTypeBuilder.tsx`
-- [ ] 3.3.2 Implement definition type selector (Compose vs Dockerfile)
-- [ ] 3.3.3 Create compose template editor (textarea with YAML highlighting)
-- [ ] 3.3.4 Create dockerfile editor (textarea with Dockerfile highlighting)
-- [ ] 3.3.5 Add build context file manager
-- [ ] 3.3.6 Add readiness probe configuration (command, timeout, interval)
-- [ ] 3.3.7 Add validation feedback (syntax check)
-- [ ] 3.3.8 Implement create/update/delete operations
-
-### 3.4 Config Editor Enhancement
-- [ ] 3.4.1 Update config form with new fields
-- [ ] 3.4.2 Add port override input (integer, 1-65535)
-- [ ] 3.4.3 Add start command input
-- [ ] 3.4.4 Add working directory input
-- [ ] 3.4.5 Create environment variables editor (key-value table)
-- [ ] 3.4.6 Create volumes editor (source/target/type table)
-- [ ] 3.4.7 Add JSON validation for env vars and volumes
-- [ ] 3.4.8 Implement tabbed sections (Basic / Runtime / Advanced)
-
-### 3.5 Config Folder Manager
-- [ ] 3.5.1 Create `components/ConfigFolderManager.tsx`
-- [ ] 3.5.2 Implement folder list view
-- [ ] 3.5.3 Create folder editor (name, description, mount_path)
-- [ ] 3.5.4 Create file manager (add/edit/delete files with path and content)
-- [ ] 3.5.5 Implement file content editor (textarea with syntax highlighting)
-- [ ] 3.5.6 Create project override manager
-- [ ] 3.5.7 Add active/inactive toggle
-- [ ] 3.5.8 Show folder size indicator
-
-### 3.6 Navigation Updates
-- [ ] 3.6.1 Update header/navigation to link to `/tool-workshop`
-- [ ] 3.6.2 Remove old `/tool-configs` and `/tool-types` routes (or redirect)
-- [ ] 3.6.3 Update breadcrumb navigation if applicable
-
-## Phase 4: Integration & Testing
-
-### 4.1 Backend Testing
-- [ ] 4.1.1 Test config folder CRUD operations
-- [ ] 4.1.2 Test config folder project overrides
-- [ ] 4.1.3 Test tool type creation with dockerfile
-- [ ] 4.1.4 Test tool type creation with compose
-- [ ] 4.1.5 Test readiness probe execution (success case)
-- [ ] 4.1.6 Test readiness probe execution (timeout case)
-- [ ] 4.1.7 Test instance creation with config folders mounted
-- [ ] 4.1.8 Test instance creation with port override
-- [ ] 4.1.9 Test instance creation with volumes
-- [ ] 4.1.10 Test 10MB size limit enforcement
-
-### 4.2 Frontend Testing
-- [ ] 4.2.1 Test Tool Workshop page load
-- [ ] 4.2.2 Test tool type creation flow
-- [ ] 4.2.3 Test config folder creation and file management
-- [ ] 4.2.4 Test config editor with all new fields
-- [ ] 4.2.5 Test responsive layout on mobile
-- [ ] 4.2.6 Test form validation (port range, JSON structure)
-
-### 4.3 End-to-End Testing
-- [ ] 4.3.1 Create a new tool type with dockerfile, start instance
-- [ ] 4.3.2 Create a new tool type with compose, start instance
-- [ ] 4.3.3 Create config folder, mount into instance, verify files present
-- [ ] 4.3.4 Add project override, verify different files in different projects
-- [ ] 4.3.5 Test readiness probe with failing command (should mark failed)
-- [ ] 4.3.6 Test readiness probe with succeeding command (should mark running)
-
-### 4.4 Quality Gates
-- [ ] 4.4.1 Run backend linting (ruff)
-- [ ] 4.4.2 Run backend type checking (mypy)
-- [ ] 4.4.3 Run frontend type checking (tsc)
-- [ ] 4.4.4 Run frontend linting (eslint)
-- [ ] 4.4.5 Build frontend and verify no errors
-- [ ] 4.4.6 Run existing tests to ensure no regressions
-- [ ] 4.4.7 Verify backward compatibility (existing instances still work)
-
-## Phase 5: Documentation & Deployment
-
-### 5.1 Documentation
-- [ ] 5.1.1 Update API documentation (OpenAPI/Swagger annotations)
-- [ ] 5.1.2 Add tool workshop user guide
-- [ ] 5.1.3 Document config folder usage
-- [ ] 5.1.4 Document readiness probe configuration
-- [ ] 5.1.5 Add example dockerfile and compose templates
-
-### 5.2 Migration & Deployment
-- [ ] 5.2.1 Verify database migrations run cleanly on existing data
-- [ ] 5.2.2 Update seed data for built-in tool types (add definition_type)
-- [ ] 5.2.3 Test fresh install (no existing data)
-- [ ] 5.2.4 Commit all changes with conventional commit messages
-- [ ] 5.2.5 Create comprehensive PR description
-
-## Quality Gates Summary
-
-**Before completing this change:**
-- All migrations must run successfully
-- Backend linting and type checking must pass
-- Frontend build must succeed with no errors
-- All new API endpoints must be tested
-- At least one end-to-end test for each new feature
-- No regressions in existing instance creation flow
-- Documentation updated
From 2a9e57ad0d90518415662a0ed7ca5528689345d7 Mon Sep 17 00:00:00 2001
From: Fusion
Date: Fri, 22 May 2026 21:01:44 +0200
Subject: [PATCH 2/6] chore: archive superseded cloudflare-tunnel-instances
OpenSpec change
This change proposed using Cloudflare API for persistent tunnels.
Superseded by temporary tunnel approach using 'cloudflared tunnel --url'
which requires no API tokens, account IDs, or DNS configuration.
---
.../.openspec.yaml | 0
.../2026-05-22-cloudflare-tunnel-instances-superseded}/design.md | 0
.../proposal.md | 0
.../specs/cloudflare-tunnel-management/spec.md | 0
.../2026-05-22-cloudflare-tunnel-instances-superseded}/tasks.md | 0
5 files changed, 0 insertions(+), 0 deletions(-)
rename openspec/changes/{cloudflare-tunnel-instances => archive/2026-05-22-cloudflare-tunnel-instances-superseded}/.openspec.yaml (100%)
rename openspec/changes/{cloudflare-tunnel-instances => archive/2026-05-22-cloudflare-tunnel-instances-superseded}/design.md (100%)
rename openspec/changes/{cloudflare-tunnel-instances => archive/2026-05-22-cloudflare-tunnel-instances-superseded}/proposal.md (100%)
rename openspec/changes/{cloudflare-tunnel-instances => archive/2026-05-22-cloudflare-tunnel-instances-superseded}/specs/cloudflare-tunnel-management/spec.md (100%)
rename openspec/changes/{cloudflare-tunnel-instances => archive/2026-05-22-cloudflare-tunnel-instances-superseded}/tasks.md (100%)
diff --git a/openspec/changes/cloudflare-tunnel-instances/.openspec.yaml b/openspec/changes/archive/2026-05-22-cloudflare-tunnel-instances-superseded/.openspec.yaml
similarity index 100%
rename from openspec/changes/cloudflare-tunnel-instances/.openspec.yaml
rename to openspec/changes/archive/2026-05-22-cloudflare-tunnel-instances-superseded/.openspec.yaml
diff --git a/openspec/changes/cloudflare-tunnel-instances/design.md b/openspec/changes/archive/2026-05-22-cloudflare-tunnel-instances-superseded/design.md
similarity index 100%
rename from openspec/changes/cloudflare-tunnel-instances/design.md
rename to openspec/changes/archive/2026-05-22-cloudflare-tunnel-instances-superseded/design.md
diff --git a/openspec/changes/cloudflare-tunnel-instances/proposal.md b/openspec/changes/archive/2026-05-22-cloudflare-tunnel-instances-superseded/proposal.md
similarity index 100%
rename from openspec/changes/cloudflare-tunnel-instances/proposal.md
rename to openspec/changes/archive/2026-05-22-cloudflare-tunnel-instances-superseded/proposal.md
diff --git a/openspec/changes/cloudflare-tunnel-instances/specs/cloudflare-tunnel-management/spec.md b/openspec/changes/archive/2026-05-22-cloudflare-tunnel-instances-superseded/specs/cloudflare-tunnel-management/spec.md
similarity index 100%
rename from openspec/changes/cloudflare-tunnel-instances/specs/cloudflare-tunnel-management/spec.md
rename to openspec/changes/archive/2026-05-22-cloudflare-tunnel-instances-superseded/specs/cloudflare-tunnel-management/spec.md
diff --git a/openspec/changes/cloudflare-tunnel-instances/tasks.md b/openspec/changes/archive/2026-05-22-cloudflare-tunnel-instances-superseded/tasks.md
similarity index 100%
rename from openspec/changes/cloudflare-tunnel-instances/tasks.md
rename to openspec/changes/archive/2026-05-22-cloudflare-tunnel-instances-superseded/tasks.md
From d5f9df33b78e5361d6ebc2db964afae0183c1fc9 Mon Sep 17 00:00:00 2001
From: Fusion
Date: Fri, 22 May 2026 21:26:05 +0200
Subject: [PATCH 3/6] feat(frontend): update sessions page for enhanced health
monitoring
- Add new status badges: starting, probing, unhealthy
- Show tunnel error only when tunnel_status is unreachable
- Show app error badge with status code for error_response
- Add collapsible probe output section for diagnostics
- Update health polling to check all active instances
- Only show Recreate Tunnel button for unreachable tunnels
---
apps/api/src/api/tool_instances.py | 159 +++++++++++++++---
apps/api/src/models/tool_instance.py | 5 +-
apps/api/src/services/docker.py | 135 +++++++++++++--
apps/web/src/api/sessions.ts | 16 +-
apps/web/src/pages/sessions.tsx | 61 ++++++-
.../instance-health-monitoring/.openspec.yaml | 2 +
.../instance-health-monitoring/design.md | 79 +++++++++
.../instance-health-monitoring/proposal.md | 29 ++++
.../specs/instance-runtime-health/spec.md | 57 +++++++
.../specs/instance-startup-health/spec.md | 83 +++++++++
.../specs/readiness-probe-integration/spec.md | 51 ++++++
.../specs/smart-tunnel-recovery/spec.md | 45 +++++
.../specs/tool-instances/spec.md | 50 ++++++
.../instance-health-monitoring/tasks.md | 56 ++++++
14 files changed, 779 insertions(+), 49 deletions(-)
create mode 100644 openspec/changes/instance-health-monitoring/.openspec.yaml
create mode 100644 openspec/changes/instance-health-monitoring/design.md
create mode 100644 openspec/changes/instance-health-monitoring/proposal.md
create mode 100644 openspec/changes/instance-health-monitoring/specs/instance-runtime-health/spec.md
create mode 100644 openspec/changes/instance-health-monitoring/specs/instance-startup-health/spec.md
create mode 100644 openspec/changes/instance-health-monitoring/specs/readiness-probe-integration/spec.md
create mode 100644 openspec/changes/instance-health-monitoring/specs/smart-tunnel-recovery/spec.md
create mode 100644 openspec/changes/instance-health-monitoring/specs/tool-instances/spec.md
create mode 100644 openspec/changes/instance-health-monitoring/tasks.md
diff --git a/apps/api/src/api/tool_instances.py b/apps/api/src/api/tool_instances.py
index 65150f3..e8805b4 100644
--- a/apps/api/src/api/tool_instances.py
+++ b/apps/api/src/api/tool_instances.py
@@ -30,11 +30,14 @@ from src.services.docker import (
execute_compose_command,
find_free_port,
get_container_id,
+ get_container_logs,
get_container_name,
+ get_container_status,
recreate_tunnel,
render_compose_template,
start_cloudflared_tunnel,
stop_cloudflared_tunnel,
+ wait_for_container_running,
write_compose_file,
write_config_files,
write_env_file,
@@ -552,20 +555,67 @@ async def start_instance(
else:
logger.warning("Failed to connect %s to backend network", container_name)
- instance.status = "starting"
- instance.last_started_at = datetime.now()
- await session.commit()
- logger.info("Instance %s container is running, checking readiness", instance.id)
-
+ # Verify container reached running state
+ if instance.container_id:
+ instance.status = "starting"
+ instance.last_started_at = datetime.now()
+ await session.commit()
+ logger.info("Instance %s: verifying container startup...", instance.id)
+
+ startup_result = wait_for_container_running(instance.container_id, timeout=30, interval=2.0)
+
+ if not startup_result["success"]:
+ # Container failed to start
+ error_msg = f"Container failed to start: status={startup_result['status']}"
+ if startup_result["exit_code"] is not None:
+ error_msg += f", exit_code={startup_result['exit_code']}"
+
+ # Get logs for debugging
+ logs = get_container_logs(instance.container_id, tail=50)
+
+ instance.status = "error"
+ await session.commit()
+ logger.error(
+ "Instance %s container startup failed after %.1fs: %s\nLogs:\n%s",
+ instance.id,
+ startup_result["waited_seconds"],
+ error_msg,
+ logs,
+ )
+ return {
+ "status": "error",
+ "error": error_msg,
+ "logs": logs,
+ }
+
+ logger.info(
+ "Instance %s container started successfully after %.1fs",
+ instance.id,
+ startup_result["waited_seconds"],
+ )
+
# Execute readiness probe if configured
tool_type = await session.get(ToolType, instance.tool_type_id)
- if tool_type and tool_type.readiness_probe:
- probe_config = tool_type.readiness_probe
- probe_command = probe_config.get("command", "")
- probe_timeout = probe_config.get("timeout", 30)
- probe_interval = probe_config.get("interval", 2)
+ if tool_type and instance.container_id:
+ # Determine probe command
+ probe_command = None
+ probe_timeout = 30
+ probe_interval = 2
- if probe_command and instance.container_id:
+ if tool_type.readiness_probe:
+ probe_config = tool_type.readiness_probe
+ probe_command = probe_config.get("command", "")
+ probe_timeout = probe_config.get("timeout", 30)
+ probe_interval = probe_config.get("interval", 2)
+ elif "web" in (tool_type.interfaces or []):
+ # Default probe for web tools
+ probe_command = f"curl -f http://localhost:{tool_type.default_port or 8080}"
+ probe_timeout = 30
+ probe_interval = 2
+
+ if probe_command:
+ instance.status = "probing"
+ await session.commit()
logger.info(
"Executing readiness probe for instance %s: command='%s', timeout=%d, interval=%d",
instance.id, probe_command, probe_timeout, probe_interval
@@ -578,14 +628,25 @@ async def start_instance(
interval=probe_interval,
)
+ # Store probe result
+ instance.probe_result = {
+ "success": success,
+ "command": probe_command,
+ "logs": probe_logs,
+ "timestamp": datetime.now().isoformat(),
+ }
+
if not success:
- instance.status = "failed"
- instance.url = None
- instance.public_url = None
+ instance.status = "unhealthy"
await session.commit()
- logger.error("Readiness probe failed for instance %s: %s", instance.id, "\n".join(probe_logs))
+ logger.error(
+ "Readiness probe failed for instance %s after %ds: %s",
+ instance.id,
+ probe_timeout,
+ "\n".join(probe_logs),
+ )
return {
- "status": "failed",
+ "status": "unhealthy",
"error": f"Readiness probe failed after {probe_timeout}s",
"probe_logs": probe_logs,
}
@@ -956,6 +1017,17 @@ async def recreate_tunnel_endpoint(
detail="instance must be running to recreate tunnel",
)
+ # Validate tunnel is actually broken before recreating
+ if instance.url:
+ tunnel_health = check_tunnel_health(instance.url)
+ if tunnel_health["tunnel_status"] == "error_response":
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=f"Tunnel is working but application returned HTTP {tunnel_health.get('status_code')}. Recreating the tunnel will not fix this issue.",
+ )
+ elif tunnel_health["tunnel_status"] == "healthy":
+ return {"status": "healthy", "url": instance.url, "message": "Tunnel is already healthy"}
+
# Get tool type for default port
tool_type = await session.get(ToolType, instance.tool_type_id)
instance_port = tool_type.default_port if tool_type and tool_type.default_port else 8080
@@ -987,8 +1059,8 @@ async def recreate_tunnel_endpoint(
@router.get(
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/health",
- summary="Check tunnel health",
- description="Check if the temporary Cloudflare tunnel for an instance is healthy.",
+ summary="Check instance health",
+ description="Check container and tunnel health for an instance.",
)
async def check_instance_tunnel_health(
project_id: uuid.UUID,
@@ -997,7 +1069,7 @@ async def check_instance_tunnel_health(
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> dict:
- """Check tunnel health for an instance.
+ """Check health for an instance (container + tunnel).
Args:
project_id: UUID of the project.
@@ -1007,7 +1079,7 @@ async def check_instance_tunnel_health(
session: Database session.
Returns:
- Dictionary with health status.
+ Dictionary with container_status, tunnel_status, probe_status, and overall healthy flag.
"""
_user = await _get_user(session, user_id)
_project = await _get_owned_project(project_id, user_id, session)
@@ -1018,11 +1090,50 @@ async def check_instance_tunnel_health(
status_code=status.HTTP_404_NOT_FOUND, detail="instance not found"
)
- if not instance.url or instance.status != "running":
- return {"healthy": False, "status_code": None, "error": "instance not running"}
+ # Check container status
+ container_info = {"status": "not_found", "exit_code": None, "health": None}
+ if instance.container_id:
+ container_info = get_container_status(instance.container_id)
- health = check_tunnel_health(instance.url)
- return health
+ # Build response
+ response = {
+ "healthy": False,
+ "container_status": container_info["status"],
+ "container_health": container_info["health"],
+ "tunnel_status": "not_applicable",
+ "tunnel_status_code": None,
+ "probe_status": "not_applicable",
+ "last_probe_output": None,
+ "error": None,
+ }
+
+ # Determine probe status
+ if instance.status == "probing":
+ response["probe_status"] = "pending"
+ elif instance.probe_result:
+ response["probe_status"] = "success" if instance.probe_result.get("success") else "failed"
+ response["last_probe_output"] = "\n".join(instance.probe_result.get("logs", []))[:500]
+
+ # Check tunnel health if instance has a URL and is web-enabled
+ if instance.url and instance.status in ("running", "unhealthy"):
+ tunnel_health = check_tunnel_health(instance.url)
+ response["tunnel_status"] = tunnel_health["tunnel_status"]
+ response["tunnel_status_code"] = tunnel_health.get("status_code")
+ if tunnel_health.get("error"):
+ response["error"] = tunnel_health["error"]
+
+ # Overall healthy only if container is running AND tunnel is healthy
+ container_healthy = container_info["status"] == "running"
+ tunnel_healthy = response["tunnel_status"] == "healthy"
+ response["healthy"] = container_healthy and tunnel_healthy
+
+ # If container is not running, override error message
+ if not container_healthy:
+ response["error"] = f"Container is {container_info['status']}"
+ if container_info["exit_code"] is not None:
+ response["error"] += f" (exit code: {container_info['exit_code']})"
+
+ return response
@router.get(
diff --git a/apps/api/src/models/tool_instance.py b/apps/api/src/models/tool_instance.py
index b556bab..162144e 100644
--- a/apps/api/src/models/tool_instance.py
+++ b/apps/api/src/models/tool_instance.py
@@ -2,7 +2,7 @@ import uuid
from datetime import datetime
from typing import TYPE_CHECKING
-from sqlalchemy import DateTime, ForeignKey, Integer, String
+from sqlalchemy import DateTime, ForeignKey, Integer, JSON, String
from sqlalchemy import Uuid as UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -62,6 +62,9 @@ class ToolInstance(UUIDPrimaryKeyMixin, TimestampMixin, Base):
last_stopped_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
+ probe_result: Mapped[dict | None] = mapped_column(
+ JSON, nullable=True
+ )
tool_type: Mapped["ToolType"] = relationship()
repository: Mapped["GitRepository"] = relationship()
diff --git a/apps/api/src/services/docker.py b/apps/api/src/services/docker.py
index bb01fee..083ed57 100644
--- a/apps/api/src/services/docker.py
+++ b/apps/api/src/services/docker.py
@@ -244,24 +244,94 @@ def connect_container_to_network(container_name: str, network_name: str = "backe
return result.returncode == 0
-def get_container_status(container_id: str) -> str:
+def get_container_status(container_id: str) -> dict[str, Any]:
"""Get the status of a Docker container.
Args:
container_id: Docker container ID
Returns:
- Container status string (running, exited, etc.)
+ Dict with 'status' (running, exited, restarting, not_found),
+ 'exit_code' (int or None), and 'health' (health status or None)
"""
result = subprocess.run(
- ["docker", "inspect", "-f", "{{.State.Status}}", container_id],
+ [
+ "docker", "inspect", "-f",
+ "{{.State.Status}}|{{.State.ExitCode}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}",
+ container_id,
+ ],
capture_output=True,
text=True,
)
- if result.returncode == 0:
- return result.stdout.strip()
- return "unknown"
+ if result.returncode != 0:
+ return {"status": "not_found", "exit_code": None, "health": None}
+
+ parts = result.stdout.strip().split("|")
+ status = parts[0] if parts else "unknown"
+ exit_code = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else None
+ health = parts[2] if len(parts) > 2 and parts[2] != "none" else None
+
+ return {"status": status, "exit_code": exit_code, "health": health}
+
+
+def wait_for_container_running(
+ container_id: str, timeout: int = 30, interval: float = 2.0
+) -> dict[str, Any]:
+ """Wait for a container to reach the running state.
+
+ Polls docker inspect until the container status is "running" or timeout.
+
+ Args:
+ container_id: Docker container ID
+ timeout: Maximum seconds to wait
+ interval: Seconds between polls
+
+ Returns:
+ Dict with 'success' (bool), 'status' (str), 'exit_code' (int or None),
+ and 'waited_seconds' (float)
+ """
+ import time
+
+ start_time = time.time()
+
+ while time.time() - start_time < timeout:
+ info = get_container_status(container_id)
+
+ if info["status"] == "running":
+ return {
+ "success": True,
+ "status": "running",
+ "exit_code": None,
+ "waited_seconds": time.time() - start_time,
+ }
+
+ if info["status"] == "exited":
+ return {
+ "success": False,
+ "status": "exited",
+ "exit_code": info["exit_code"],
+ "waited_seconds": time.time() - start_time,
+ }
+
+ if info["status"] == "not_found":
+ return {
+ "success": False,
+ "status": "not_found",
+ "exit_code": None,
+ "waited_seconds": time.time() - start_time,
+ }
+
+ time.sleep(interval)
+
+ # Timeout reached
+ info = get_container_status(container_id)
+ return {
+ "success": False,
+ "status": info["status"],
+ "exit_code": info["exit_code"],
+ "waited_seconds": time.time() - start_time,
+ }
def get_container_logs(container_id: str, tail: int = 100) -> str:
@@ -424,14 +494,15 @@ def recreate_tunnel(
def check_tunnel_health(url: str, timeout: int = 10) -> dict[str, Any]:
- """Check if a tunnel URL is healthy.
+ """Check if a tunnel URL is healthy with smart error classification.
Args:
url: The tunnel URL to check
timeout: Request timeout in seconds
Returns:
- Dict with 'healthy' (bool) and 'status_code' (int or None)
+ Dict with 'tunnel_status' (healthy, unreachable, error_response, not_applicable),
+ 'status_code' (int or None), 'healthy' (bool), and 'error' (str or None)
"""
import subprocess
@@ -444,13 +515,49 @@ def check_tunnel_health(url: str, timeout: int = 10) -> dict[str, Any]:
timeout=timeout + 5,
)
status_code = int(result.stdout.strip())
+
+ if 200 <= status_code < 400:
+ return {
+ "tunnel_status": "healthy",
+ "status_code": status_code,
+ "healthy": True,
+ "error": None,
+ }
+ elif status_code in (502, 503, 504):
+ # Application error, not tunnel error
+ return {
+ "tunnel_status": "error_response",
+ "status_code": status_code,
+ "healthy": False,
+ "error": f"Application returned HTTP {status_code}",
+ }
+ else:
+ return {
+ "tunnel_status": "error_response",
+ "status_code": status_code,
+ "healthy": False,
+ "error": f"HTTP {status_code}",
+ }
+ except subprocess.TimeoutExpired:
return {
- "healthy": 200 <= status_code < 400,
- "status_code": status_code,
- }
- except (ValueError, subprocess.TimeoutExpired, Exception) as e:
- return {
- "healthy": False,
+ "tunnel_status": "unreachable",
"status_code": None,
+ "healthy": False,
+ "error": "Tunnel request timed out",
+ }
+ except (ValueError, Exception) as e:
+ error_str = str(e).lower()
+ # Classify connection errors
+ if any(err in error_str for err in ["connection refused", "econnrefused", "could not resolve", "nodename"]):
+ return {
+ "tunnel_status": "unreachable",
+ "status_code": None,
+ "healthy": False,
+ "error": f"Tunnel unreachable: {e}",
+ }
+ return {
+ "tunnel_status": "unreachable",
+ "status_code": None,
+ "healthy": False,
"error": str(e),
}
diff --git a/apps/web/src/api/sessions.ts b/apps/web/src/api/sessions.ts
index a404760..bedb600 100644
--- a/apps/web/src/api/sessions.ts
+++ b/apps/web/src/api/sessions.ts
@@ -25,6 +25,8 @@ export interface Session {
project_id: string;
status: string;
url: string | null;
+ container_status?: string;
+ probe_status?: string;
}
export async function listInstances(
@@ -101,11 +103,23 @@ export async function getUserSessions(): Promise {
return response.data.sessions;
}
+export interface InstanceHealth {
+ healthy: boolean;
+ container_status: string;
+ container_health?: string;
+ container_exit_code?: number | null;
+ tunnel_status: string;
+ tunnel_status_code: number | null;
+ probe_status: string;
+ last_probe_output?: string;
+ error?: string;
+}
+
export async function checkInstanceHealth(
projectId: string,
repoId: string,
instanceId: string
-): Promise<{ healthy: boolean; status_code: number | null; error?: string }> {
+): Promise {
const response = await apiClient.get(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/health`
);
diff --git a/apps/web/src/pages/sessions.tsx b/apps/web/src/pages/sessions.tsx
index a548d19..fe44e91 100644
--- a/apps/web/src/pages/sessions.tsx
+++ b/apps/web/src/pages/sessions.tsx
@@ -40,8 +40,18 @@ export const SessionsPage = () => {
const [deleteConfirmId, setDeleteConfirmId] = useState(null);
const [stopConfirmId, setStopConfirmId] = useState(null);
- const [tunnelHealth, setTunnelHealth] = useState>({});
+ const [tunnelHealth, setTunnelHealth] = useState>({});
const [recreatingId, setRecreatingId] = useState(null);
+ const [expandedProbeId, setExpandedProbeId] = useState(null);
const loadSessions = useCallback(async () => {
setStatus("loading");
@@ -86,13 +96,13 @@ export const SessionsPage = () => {
void loadToolTypes();
}, []);
- // Poll tunnel health every 30 seconds for running instances
+ // Poll health every 30 seconds for active instances
useEffect(() => {
const checkHealth = async () => {
- const runningSessions = sessions.filter(
- (s) => s.status === "running" && s.url
+ const activeSessions = sessions.filter(
+ (s) => ["running", "starting", "unhealthy", "probing"].includes(s.status)
);
- for (const session of runningSessions) {
+ for (const session of activeSessions) {
try {
const health = await checkInstanceHealth(
session.project_id,
@@ -106,7 +116,14 @@ export const SessionsPage = () => {
} catch {
setTunnelHealth((prev) => ({
...prev,
- [session.id]: { healthy: false, status_code: null, error: "check failed" },
+ [session.id]: {
+ healthy: false,
+ container_status: "unknown",
+ tunnel_status: "unreachable",
+ tunnel_status_code: null,
+ probe_status: "unknown",
+ error: "check failed",
+ },
}));
}
}
@@ -135,7 +152,7 @@ export const SessionsPage = () => {
}, [selectedProject]);
const activeSessions = useMemo(
- () => sessions.filter((s) => ["running", "building", "pending"].includes(s.status)),
+ () => sessions.filter((s) => ["running", "building", "pending", "starting", "probing", "unhealthy"].includes(s.status)),
[sessions]
);
@@ -328,9 +345,35 @@ export const SessionsPage = () => {
)}
{session.status}
- {tunnelHealth[session.id] && !tunnelHealth[session.id].healthy && (
+ {session.status === "starting" && (
+ starting...
+ )}
+ {session.status === "probing" && (
+ checking...
+ )}
+ {tunnelHealth[session.id] && tunnelHealth[session.id].tunnel_status === "unreachable" && (
tunnel error
)}
+ {tunnelHealth[session.id] && tunnelHealth[session.id].tunnel_status === "error_response" && (
+ app error ({tunnelHealth[session.id].tunnel_status_code})
+ )}
+ {tunnelHealth[session.id]?.last_probe_output && (
+
+
+ {expandedProbeId === session.id && (
+
+ {tunnelHealth[session.id].last_probe_output}
+
+ )}
+
+ )}
{session.url ? (
@@ -353,7 +396,7 @@ export const SessionsPage = () => {
Open
)}
- {tunnelHealth[session.id] && !tunnelHealth[session.id].healthy && (
+ {tunnelHealth[session.id] && tunnelHealth[session.id].tunnel_status === "unreachable" && (