diff --git a/.pi-map.index.md b/.pi-map.index.md index 6c615df..bda292b 100644 --- a/.pi-map.index.md +++ b/.pi-map.index.md @@ -16,7 +16,7 @@ dir: . Trust boundary: index routes, map orients, source decides. ## role -Infrastructure and deployment configuration package for a self-hosted project management platform with containerized services, reverse proxy support, and AI agent workflow definitions. +Infrastructure and deployment configuration package for a self-hosted project management platform with multi-service Docker orchestration, SSO integration, and development tooling. ## parent - ## children diff --git a/.pi-map.md b/.pi-map.md index 451d643..957ae7e 100644 --- a/.pi-map.md +++ b/.pi-map.md @@ -18,7 +18,7 @@ index: ./.pi-map.index.md Trust boundary: index routes, map orients, source decides. ## role -Infrastructure and deployment configuration package for a self-hosted project management platform with containerized services, reverse proxy support, and AI agent workflow definitions. +Infrastructure and deployment configuration package for a self-hosted project management platform with multi-service Docker orchestration, SSO integration, and development tooling. ## files - .env.example | Provides a template of environment variables for configuring a Headquarter application with PostgreSQL, Redis, Authentik SSO, and Docker/Traefik deployment - .gitignore | Specifies files and directories for Git to ignore across a multi-language project with Python, Node, and custom tooling | dep: Git @@ -31,7 +31,7 @@ Infrastructure and deployment configuration package for a self-hosted project ma - progress.md | Tracks completed and remaining tasks for a backend-frontend code refactoring project organized in 7 phases - swap-pane | Empty file with no functionality ## arch -Docker Compose-based microservices architecture with PostgreSQL/Redis backend, separate API/frontend containers, Traefik reverse proxy integration, environment-driven configuration, and OpenSpec-driven AI agent orchestration. +Containerized microservices architecture using Docker Compose with separate PostgreSQL/Redis data layer, API backend, and web frontend services, fronted by Traefik reverse proxy with TLS termination, environment-driven configuration, and Makefile-based lifecycle management. ## tags docker, redis, git, application, postgresql, compose, traefik, project ## symbols diff --git a/apps/.pi-map.index.md b/apps/.pi-map.index.md index a814c2f..fe888c5 100644 --- a/apps/.pi-map.index.md +++ b/apps/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps ## role -Contains the main deployable application modules that compose the complete system. +Contains the main application entry points and executable modules for the project. ## parent index: ./.pi-map.index.md map: ./.pi-map.md diff --git a/apps/.pi-map.md b/apps/.pi-map.md index 132ccd6..3b33ada 100644 --- a/apps/.pi-map.md +++ b/apps/.pi-map.md @@ -4,10 +4,10 @@ dir: apps index: apps/.pi-map.index.md ## role -Contains the main deployable application modules that compose the complete system. +Contains the main application entry points and executable modules for the project. ## files ## arch -Modular application architecture with separate entry points for different application contexts or deployment targets. +Modular application structure with separate deployable units, likely following a microservices or monorepo pattern with distinct apps sharing common libraries. ## tags - ## symbols diff --git a/apps/api/.pi-map.index.md b/apps/api/.pi-map.index.md index 52afd12..35477e3 100644 --- a/apps/api/.pi-map.index.md +++ b/apps/api/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/api ## role -Self-hosted FastAPI backend API that manages projects, git repositories, and development tools via Docker instances. +Backend API service that provides a self-hosted FastAPI server for managing development projects, git repositories, and containerized tools via Docker. ## parent index: apps/.pi-map.index.md map: apps/.pi-map.md diff --git a/apps/api/.pi-map.md b/apps/api/.pi-map.md index 4669240..f3e1437 100644 --- a/apps/api/.pi-map.md +++ b/apps/api/.pi-map.md @@ -4,7 +4,7 @@ dir: apps/api index: apps/api/.pi-map.index.md ## role -Self-hosted FastAPI backend API that manages projects, git repositories, and development tools via Docker instances. +Backend API service that provides a self-hosted FastAPI server for managing development projects, git repositories, and containerized tools via Docker. ## files - .dockerignore | Specifies files and directories to exclude from Docker build context to reduce image size and avoid copying unnecessary files into containers. | dep: Docker - Dockerfile | Multi-stage Docker build for a Python application with Docker socket access, Cloudflare tunneling, and database dependency waiting | dep: python:3.11-slim, gcc, libpq-dev, docker-ce-cli, docker-compose-plugin, cloudflared, uvicorn, pyproject.toml dependencies @@ -14,7 +14,7 @@ Self-hosted FastAPI backend API that manages projects, git repositories, and dev - uv.lock | Lock file for the uv Python package manager that pins exact dependency versions and their artifact hashes for reproducible installations | dep: uv, Python 3.11+, aiosqlite, alembic, annotated-doc, annotated-types, anyio, ast-serialize, asyncpg, and many other PyPI packages - wait-for-db.sh | Wait for a PostgreSQL database to become available before executing a command, with configurable retry logic. | dep: nc (netcat), sh (POSIX shell), sleep ## arch -Multi-stage Dockerized Python application using async FastAPI, PostgreSQL with Alembic migrations, uv package management, and Cloudflare tunneling with external Docker socket access. +Modern Python async architecture using FastAPI with SQLAlchemy/alembic for PostgreSQL, uv for dependency management, multi-stage Docker builds with Cloudflare tunneling, and health-check orchestration for database readiness. ## tags docker, alembic, python, database, fastapi, postgresql, asyncpg, uvicorn ## symbols diff --git a/apps/api/src/.pi-map.index.md b/apps/api/src/.pi-map.index.md index 7b141b2..fc19fea 100644 --- a/apps/api/src/.pi-map.index.md +++ b/apps/api/src/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/api/src ## role -Core application package for the Headquarter API backend service, providing configuration, database connectivity, structured logging, and FastAPI application orchestration. +Core application package that bootstraps and configures the Headquarter API FastAPI service with its infrastructure concerns. ## parent index: apps/api/.pi-map.index.md map: apps/api/.pi-map.md @@ -13,6 +13,9 @@ map: apps/api/.pi-map.md - apps/api/src/auth index: apps/api/src/auth/.pi-map.index.md map: apps/api/src/auth/.pi-map.md +- apps/api/src/headquarter_api.egg-info + index: apps/api/src/headquarter_api.egg-info/.pi-map.index.md + map: apps/api/src/headquarter_api.egg-info/.pi-map.md - apps/api/src/models index: apps/api/src/models/.pi-map.index.md map: apps/api/src/models/.pi-map.md @@ -46,6 +49,6 @@ map: apps/api/src/.pi-map.md - change src config read: config.py, logging_config.py - explore src subdirectories - index: apps/api/src/api/.pi-map.index.md, apps/api/src/auth/.pi-map.index.md, apps/api/src/models/.pi-map.index.md + index: apps/api/src/api/.pi-map.index.md, apps/api/src/auth/.pi-map.index.md, apps/api/src/headquarter_api.egg-info/.pi-map.index.md ## dirty - diff --git a/apps/api/src/.pi-map.md b/apps/api/src/.pi-map.md index ea0c992..6fd375f 100644 --- a/apps/api/src/.pi-map.md +++ b/apps/api/src/.pi-map.md @@ -4,15 +4,15 @@ dir: apps/api/src index: apps/api/src/.pi-map.index.md ## role -Core application package for the Headquarter API backend service, providing configuration, database connectivity, structured logging, and FastAPI application orchestration. +Core application package that bootstraps and configures the Headquarter API FastAPI service with its infrastructure concerns. ## files - __init__.py | Marks the directory as a Python package for the Headquarter API. - config.py | Defines application configuration settings using Pydantic with environment variable loading, database URL construction, and computed properties for service URLs and OAuth endpoints. | exp: class:Settings, func:build_database_url(user: str, password: str, host: str, port: int, database: str) → str | dep: pydantic, pydantic_settings - database.py | Configures async SQLAlchemy database engine and session factory, and provides retry logic for database initialization with Alembic migrations via subprocess. | exp: func:init_database(max_retries, retry_delay) → bool, call:range, call:engine.connect, call:test_conn.execute, call:text, call:test_conn.close, call:logger.info, call:asyncio.get_event_loop().run_in_executor, call:subprocess.run, call:logger.debug, call:logger.error, call:asyncio.sleep, call:str(exc).lower, call:logger.warning | dep: asyncio, logging, subprocess, sqlalchemy.ext.asyncio, sqlalchemy.pool, src.config, sqlalchemy - logging_config.py | Configures structured JSON logging with correlation ID injection, custom formatters, and HTTP request/exception middleware for a FastAPI application. | exp: class:CorrelationIdFilter, method:filter(self, record: logging.LogRecord) → bool, call:get_correlation_id, class:JSONFormatter, method:format(self, record: logging.LogRecord) → str, call:self.formatTime, call:record.getMessage, call:getattr, call:self.formatException, call:json.dumps, method:formatTime(self, record: logging.LogRecord, datefmt) → str, call:time.strftime, call:time.gmtime, class:RequestLoggingMiddleware, method:dispatch(self, request: Request, call_next: Callable) → Response, call:time.time, call:logger.info, call:call_next, call:int, call:logger.error, call:type, call:traceback.format_exc, class:ExceptionLoggingMiddleware, method:dispatch(self, request: Request, call_next: Callable) → Response, call:call_next, call:logger.critical, call:traceback.format_exc, func:configure_logging(level) → None, call:JSONFormatter, call:logging.StreamHandler, call:console_handler.setFormatter, call:console_handler.addFilter, call:CorrelationIdFilter, call:root_logger.setLevel, call:logging.getLogger("uvicorn").setLevel, call:logging.getLogger("uvicorn.access").setLevel, call:logging.getLogger("sqlalchemy.engine").setLevel, call:logger.info, call:logging.getLevelName | dep: json, logging, sys, time, traceback, collections.abc, fastapi, starlette.middleware.base, src.services.shared.correlation -- main.py | FastAPI application entry point that initializes and configures the Headquarter API with routers, middleware, database, health monitoring, and CORS | exp: func:_sanitize_validation_errors(errors), call:error.get, call:str, call:ctx.items, call:isinstance, call:type, call:sanitized.append, func:validation_exception_handler(request: Request, exc: RequestValidationError), call:exc.errors, call:logger.warning, call:_sanitize_validation_errors, call:JSONResponse, func:on_startup(), call:logger.info, call:init_database, call:logger.error, call:sys.exit, call:_health_monitor.start, call:seed_builtin_tool_types, func:on_shutdown(), call:logger.info, call:_health_monitor.stop | dep: logging, os, fastapi, fastapi.exceptions, fastapi.middleware.cors, fastapi.responses, fastapi.staticfiles, src.api.config, src.api.project, src.api.system, src.api.tool, src.api.user, src.api.workspace, src.config, src.models, src.database, src.logging_config, src.seeds.builtin_tool_types, src.services.instance, src.services.shared, sys, src.api.* +- main.py | Initializes and configures a FastAPI application for the "Headquarter API" with database setup, middleware, routing, and background services. | exp: func:_sanitize_validation_errors(errors), call:error.get, call:str, call:ctx.items, call:isinstance, call:type, call:sanitized.append, func:validation_exception_handler(request: Request, exc: RequestValidationError), call:exc.errors, call:logger.warning, call:_sanitize_validation_errors, call:JSONResponse, func:on_startup(), call:logger.info, call:init_database, call:logger.error, call:sys.exit, call:_health_monitor.start, call:seed_builtin_tool_types, func:on_shutdown(), call:logger.info, call:_health_monitor.stop | dep: logging, os, fastapi, fastapi.exceptions, fastapi.middleware.cors, fastapi.responses, fastapi.staticfiles, src.api.config, src.api.project, src.api.system, src.api.tool, src.api.user, src.api.workspace, src.config, src.models, src.database, src.logging_config, src.seeds.builtin_tool_types, src.services.instance, src.services.shared, sys, src.api.* ## arch -Layered architecture with Pydantic-based configuration management, async SQLAlchemy with Alembic migrations, structured JSON logging with correlation ID tracing, and modular FastAPI setup with middleware pipeline and health monitoring. +Layered configuration with Pydantic settings, async SQLAlchemy with Alembic migration integration, structured JSON logging with correlation ID tracking, and FastAPI middleware/routing setup. ## tags src, database, logging, call:logger.info, api, middleware, fastapi, filter ## symbols @@ -30,6 +30,6 @@ src, database, logging, call:logger.info, api, middleware, fastapi, filter - change src config read: config.py, logging_config.py - explore src subdirectories - index: apps/api/src/api/.pi-map.index.md, apps/api/src/auth/.pi-map.index.md, apps/api/src/models/.pi-map.index.md + index: apps/api/src/api/.pi-map.index.md, apps/api/src/auth/.pi-map.index.md, apps/api/src/headquarter_api.egg-info/.pi-map.index.md ## dirty - diff --git a/apps/api/src/api/.pi-map.index.md b/apps/api/src/api/.pi-map.index.md index 9ca26c1..1ed61c7 100644 --- a/apps/api/src/api/.pi-map.index.md +++ b/apps/api/src/api/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/api/src/api ## role -Defines reusable Pydantic validators for API request/response schema validation across the API layer. +Defines reusable API validation utilities and package structure for FastAPI router organization. ## parent index: apps/api/src/.pi-map.index.md map: apps/api/src/.pi-map.md diff --git a/apps/api/src/api/.pi-map.md b/apps/api/src/api/.pi-map.md index 9e60eec..66417ff 100644 --- a/apps/api/src/api/.pi-map.md +++ b/apps/api/src/api/.pi-map.md @@ -4,12 +4,12 @@ dir: apps/api/src/api index: apps/api/src/api/.pi-map.index.md ## role -Defines reusable Pydantic validators for API request/response schema validation across the API layer. +Defines reusable API validation utilities and package structure for FastAPI router organization. ## files - __init__.py | Marks the directory as a Python package for API routers. - shared_validators.py | Provides reusable Pydantic validator functions for API schema validation including mount paths, files, environment variables, and volume mounts. | exp: func:validate_mount_path(v: str | None) → str | None, call:v.startswith, raise:ValueError, func:validate_files(v: dict | None, max_size_bytes) → dict | None, call:v.items, call:path.startswith, call:len, call:content.encode, raise:ValueError, func:validate_env_vars(v: dict | None) → dict | None, call:isinstance, raise:ValueError, func:validate_volumes(v: list | None) → list | None, call:isinstance, call:enumerate, raise:ValueError ## arch -Utility module pattern providing shared, composable validation functions using Pydantic's validator system for common container/docker-like domain objects (mounts, volumes, files, env vars). +Modular utility package with shared Pydantic validators for cross-cutting API schema concerns, following separation of validation logic from route handlers. ## tags validate, raise:value, error, call:isinstance, mount, api, init, path ## symbols diff --git a/apps/api/src/api/tool/.pi-map.index.md b/apps/api/src/api/tool/.pi-map.index.md index 294c810..da8b35e 100644 --- a/apps/api/src/api/tool/.pi-map.index.md +++ b/apps/api/src/api/tool/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/api/src/api/tool ## role -Provides FastAPI REST endpoints for managing the complete lifecycle of containerized tools, including type definitions, instances, sessions, and validation. +Provides FastAPI REST API endpoints and routers for managing the complete lifecycle of Docker-based tools, including type definitions, instances, sessions, and lifecycle operations. ## parent index: apps/api/src/api/.pi-map.index.md map: apps/api/src/api/.pi-map.md @@ -13,6 +13,7 @@ map: apps/api/src/api/.pi-map.md - sessions.py - tool_definitions.py - tool_instances.py +- tool_lifecycle.py - tool_types.py - tool_types_validation.py ## links diff --git a/apps/api/src/api/tool/.pi-map.md b/apps/api/src/api/tool/.pi-map.md index 56284ba..4192232 100644 --- a/apps/api/src/api/tool/.pi-map.md +++ b/apps/api/src/api/tool/.pi-map.md @@ -4,16 +4,17 @@ dir: apps/api/src/api/tool index: apps/api/src/api/tool/.pi-map.index.md ## role -Provides FastAPI REST endpoints for managing the complete lifecycle of containerized tools, including type definitions, instances, sessions, and validation. +Provides FastAPI REST API endpoints and routers for managing the complete lifecycle of Docker-based tools, including type definitions, instances, sessions, and lifecycle operations. ## files -- __init__.py | Aggregates and exports tool-related API routers from submodules for centralized access | dep: src.api.tool.sessions, src.api.tool.tool_definitions, src.api.tool.tool_instances, src.api.tool.tool_types -- sessions.py | Provides a FastAPI endpoint to retrieve all active tool sessions (running instances) for the currently authenticated user with related metadata. | exp: func:get_user_sessions(user_id, session) → dict, call:_get_user, call:session.execute, call:select(ToolInstance) .where(ToolInstance.owner_id == user_id) .where( ToolInstance.status.in_( ["running", "building", "pending", "stopped", "error"] ) ) .order_by, call:ToolInstance.status.in_, call:ToolInstance.created_at.desc, call:result.scalars().all, call:session.get, call:sessions.append, call:str, call:instance.created_at.isoformat | dep: uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models +- __init__.py | Aggregates and exports all tool-related API routers from submodules as a unified module interface. | dep: src.api.tool.sessions, src.api.tool.tool_definitions, src.api.tool.tool_instances, src.api.tool.tool_lifecycle, src.api.tool.tool_types +- sessions.py | API endpoint that retrieves all active tool instances (sessions) for the currently authenticated user with related metadata | exp: func:get_user_sessions(user_id, session) → dict, call:_get_user, call:session.execute, call:select(ToolInstance) .where(ToolInstance.owner_id == user_id) .where( ToolInstance.status.in_( ["running", "building", "pending", "stopped", "error"] ) ) .order_by, call:ToolInstance.status.in_, call:ToolInstance.created_at.desc, call:result.scalars().all, call:session.get, call:sessions.append, call:str, call:instance.created_at.isoformat | dep: uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models - tool_definitions.py | FastAPI router providing CRUD endpoints and manifest compilation for tool definition manifests stored in a database. | exp: class:CreateToolDefinitionRequest, class:UpdateToolDefinitionRequest, func:create_tool_definition(data: CreateToolDefinitionRequest, user_id, session) → dict, call:uuid.UUID, call:session.get, call:session.execute, call:select(ToolDefinitionManifest).where, call:existing.scalar_one_or_none, call:ToolDefinitionManifest, call:session.add, call:session.commit, call:session.refresh, call:logger.info, call:str, call:definition.created_at.isoformat, raise:HTTPException, func:list_tool_definitions(user_id, session, include_bases) → dict, call:select, call:query.where, call:ToolDefinitionManifest.is_base.is_, call:session.execute, call:query.order_by, call:ToolDefinitionManifest.created_at.desc, call:result.scalars().all, call:str, call:d.created_at.isoformat, func:get_tool_definition(definition_id: uuid.UUID, user_id, session) → dict, call:session.get, call:str, call:definition.created_at.isoformat, call:definition.updated_at.isoformat, raise:HTTPException, func:update_tool_definition(definition_id: uuid.UUID, data: UpdateToolDefinitionRequest, user_id, session) → dict, call:session.get, call:session.commit, call:session.refresh, call:logger.info, call:str, call:definition.updated_at.isoformat, raise:HTTPException, func:delete_tool_definition(definition_id: uuid.UUID, user_id, session) → dict, call:session.get, call:session.execute, call:select(ToolType).where, call:result.scalars().all, call:", ".join, call:session.delete, call:session.commit, call:logger.info, call:str, raise:HTTPException, func:compile_tool_definition(definition_id: uuid.UUID, user_id, session) → dict, call:session.get, call:dict, call:resolve_base, call:deep_merge, call:compile_dockerfile, call:compile_entrypoint, call:compute_image_tag, call:compile_compose, call:session.commit, call:str, raise:HTTPException | dep: logging, uuid, fastapi, pydantic, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.services.build.manifest_compiler -- tool_instances.py | Defines FastAPI REST endpoints for managing Docker-based tool instances within projects, including CRUD operations, lifecycle control (start/stop/restart), logs, health checks, and tunnel management. | exp: func:create_instance(project_id: uuid.UUID, repo_id: uuid.UUID, data: CreateInstanceRequest, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:create_tool_instance, call:str, call:instance.created_at.isoformat, raise:HTTPException, func:list_instances(project_id: uuid.UUID, repo_id: uuid.UUID, user_id, session) → list[dict], call:_get_user, call:_get_owned_project, call:session.execute, call:select(ToolInstance) .where(ToolInstance.repository_id == repo_id) .where(ToolInstance.owner_id == user_id) .order_by, call:ToolInstance.created_at.desc, call:result.scalars().all, call:str, call:i.created_at.isoformat, func:get_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:session.get, call:get_container_status, call:str, call:instance.created_at.isoformat, call:instance.last_started_at.isoformat, raise:HTTPException, func:rename_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:data.get("display_name", "").strip, call:rename_tool_instance, call:str, raise:HTTPException, func:start_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, data, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:start_tool_instance, raise:HTTPException, func:stop_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:stop_tool_instance, raise:HTTPException, func:restart_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:restart_tool_instance, raise:HTTPException, func:delete_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, force, user_id, session) → None, call:_get_user, call:_get_owned_project, call:delete_tool_instance, call:str, call:detail.lower, raise:HTTPException, func:get_instance_logs(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, tail, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:session.get, call:get_container_logs, raise:HTTPException, func:recreate_tunnel_endpoint(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:recreate_instance_tunnel, raise:HTTPException, func:check_instance_tunnel_health(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:session.get, call:get_container_status, call:instance.probe_result.get, call:"\n".join, call:check_tunnel_health, call:tunnel_health.get, raise:HTTPException, func:get_instance_events(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, limit, user_id, session) → list[dict], call:_get_user, call:_get_owned_project, call:session.get, call:session.execute, call:select(InstanceEvent) .where(InstanceEvent.instance_id == instance_id) .order_by(InstanceEvent.created_at.desc()) .limit, call:InstanceEvent.created_at.desc, call:result.scalars().all, call:str, call:row.created_at.isoformat, raise:HTTPException, func:proxy_to_instance(request: Request, project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, path, user_id, session) → Response, call:session.get, call:str, call:dict, call:headers.pop, call:httpx.AsyncClient, call:request.body, call:client.request, call:logger.error, call:response_headers.pop, call:Response, raise:HTTPException | dep: logging, uuid, httpx, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.services.docker, src.services.shared.tunnel, src.schemas.tool, src.services.tool.instance_service +- tool_instances.py | Provides FastAPI REST endpoints for managing tool instances including CRUD operations, logs, health checks, tunnel recreation, event history, and HTTP proxying to running containers. | exp: func:create_instance(project_id: uuid.UUID, repo_id: uuid.UUID, data: CreateInstanceRequest, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:create_tool_instance, call:str, call:instance.created_at.isoformat, raise:HTTPException, func:list_instances(project_id: uuid.UUID, repo_id: uuid.UUID, user_id, session) → list[dict], call:_get_user, call:_get_owned_project, call:session.execute, call:select(ToolInstance) .where(ToolInstance.repository_id == repo_id) .where(ToolInstance.owner_id == user_id) .order_by, call:ToolInstance.created_at.desc, call:result.scalars().all, call:str, call:i.created_at.isoformat, func:get_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:session.get, call:get_container_status, call:str, call:instance.created_at.isoformat, call:instance.last_started_at.isoformat, raise:HTTPException, func:rename_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:data.get("display_name", "").strip, call:rename_tool_instance, call:str, raise:HTTPException, func:get_instance_logs(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, tail, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:session.get, call:get_container_logs, raise:HTTPException, func:recreate_tunnel_endpoint(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:recreate_instance_tunnel, raise:HTTPException, func:check_instance_tunnel_health(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:session.get, call:get_container_status, call:instance.probe_result.get, call:"\n".join, call:check_tunnel_health, call:tunnel_health.get, raise:HTTPException, func:get_instance_events(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, limit, user_id, session) → list[dict], call:_get_user, call:_get_owned_project, call:session.get, call:session.execute, call:select(InstanceEvent) .where(InstanceEvent.instance_id == instance_id) .order_by(InstanceEvent.created_at.desc()) .limit, call:InstanceEvent.created_at.desc, call:result.scalars().all, call:str, call:row.created_at.isoformat, raise:HTTPException, func:proxy_to_instance(request: Request, project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, path, user_id, session) → Response, call:session.get, call:str, call:dict, call:headers.pop, call:httpx.AsyncClient, call:request.body, call:client.request, call:logger.error, call:response_headers.pop, call:Response, raise:HTTPException | dep: logging, uuid, httpx, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.services.docker, src.services.shared.tunnel, src.schemas.tool, src.services.tool.instance_service +- tool_lifecycle.py | FastAPI router providing REST endpoints for managing Docker-based tool instance lifecycle operations (start, stop, restart, delete). | exp: func:start_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, data, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:start_tool_instance, raise:HTTPException, func:stop_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:stop_tool_instance, raise:HTTPException, func:restart_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:restart_tool_instance, raise:HTTPException, func:delete_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, force, user_id, session) → None, call:_get_user, call:_get_owned_project, call:delete_tool_instance, call:str, call:detail.lower, raise:HTTPException | dep: logging, uuid, fastapi, sqlalchemy.ext.asyncio, src.auth.dependencies, src.schemas.tool, src.services.tool.instance_service - tool_types.py | Defines FastAPI routes for CRUD operations and validation of Docker tool types with compose/dockerfile/manifest definitions. | exp: func:_require_admin(user: User) → None, func:create_tool_type(data: ToolTypeCreate, user_id, session) → ToolType, call:_get_user, call:_require_admin, call:session.scalar, call:select(ToolType).where, call:ToolType, call:session.add, call:session.commit, call:session.refresh, raise:HTTPException, func:list_tool_types(user_id, session) → list[ToolType], call:_get_user, call:session.execute, call:select(ToolType).order_by, call:list, call:result.scalars().all, func:get_tool_type(tool_type_id: uuid.UUID, user_id, session) → ToolType, call:_get_user, call:session.get, raise:HTTPException, func:update_tool_type(tool_type_id: uuid.UUID, data: ToolTypeUpdate, user_id, session) → ToolType, call:_get_user, call:_require_admin, call:session.get, call:data.model_dump, call:update_data.get, call:validate_compose_yaml, call:check_port_exposed, call:validate_required_variables, call:update_data.items, call:setattr, call:session.commit, call:session.refresh, raise:HTTPException, func:validate_tool_type_template(data: ToolTypeValidateRequest, user_id, session) → dict, call:_get_user, call:errors.append, call:validate_compose_yaml, call:str, call:data.dockerfile_template.strip().startswith, call:len, func:validate_tool_type(tool_type_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:session.get, call:errors.append, call:validate_compose_yaml, call:str, call:tool_type.dockerfile_template.strip().startswith, call:len, raise:HTTPException, func:delete_tool_type(tool_type_id: uuid.UUID, user_id, session) → None, call:_get_user, call:_require_admin, call:session.get, call:session.delete, call:session.commit, raise:HTTPException | dep: uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.api.tool.tool_types_validation, src.auth.dependencies, src.models, src.models.user, src.schemas.tool - tool_types_validation.py | Validates Docker Compose YAML templates by sanitizing template variables, parsing YAML, checking required structure, verifying port exposure, and ensuring required variables are present. | exp: func:sanitize_template_vars(template: str) → str, call:re.sub, func:validate_compose_yaml(template: str) → dict, call:sanitize_template_vars, call:yaml.safe_load, call:isinstance, raise:ValueError, func:check_port_exposed(parsed: dict, port: int) → bool, call:str, call:isinstance, call:parsed["services"].values, func:validate_required_variables(template: str, variables: list[str]) → None, raise:HTTPException | dep: re, yaml, fastapi, fastapi.HTTPException, fastapi.status ## arch -Modular router composition with domain-driven separation (types/instances/sessions/definitions), Docker-centric architecture using compose/dockerfile manifests, and validation-layer pattern for YAML sanitization and structural verification. +Modular FastAPI router composition pattern with separated concerns across CRUD operations, validation, lifecycle management, and proxying, aggregated through `__init__.py` for unified module interface. ## tags get, call:, tool, raise:httpexception, user, instance, call:str, call:session.get ## symbols diff --git a/apps/api/src/api/tool/__init__.py b/apps/api/src/api/tool/__init__.py index f525de3..f80f21a 100644 --- a/apps/api/src/api/tool/__init__.py +++ b/apps/api/src/api/tool/__init__.py @@ -3,11 +3,13 @@ from src.api.tool.sessions import sessions_router from src.api.tool.tool_definitions import router as tool_definitions_router from src.api.tool.tool_instances import router as tool_instances_router +from src.api.tool.tool_lifecycle import router as tool_lifecycle_router from src.api.tool.tool_types import router as tool_types_router __all__ = [ "sessions_router", "tool_definitions_router", "tool_instances_router", + "tool_lifecycle_router", "tool_types_router", ] diff --git a/apps/api/src/api/tool/tool_instances.py b/apps/api/src/api/tool/tool_instances.py index f17231e..54e3cd0 100644 --- a/apps/api/src/api/tool/tool_instances.py +++ b/apps/api/src/api/tool/tool_instances.py @@ -24,15 +24,11 @@ from src.auth.dependencies import ( from src.models import ToolInstance from src.services.docker import get_container_logs, get_container_status from src.services.shared.tunnel import check_tunnel_health -from src.schemas.tool import CreateInstanceRequest, StartInstanceRequest +from src.schemas.tool import CreateInstanceRequest from src.services.tool.instance_service import ( create_tool_instance, - delete_tool_instance, recreate_instance_tunnel, rename_tool_instance, - restart_tool_instance, - start_tool_instance, - stop_tool_instance, ) logger = logging.getLogger(__name__) @@ -190,116 +186,6 @@ async def rename_instance( } -@router.post( - "/{project_id}/repositories/{repo_id}/instances/{instance_id}/start", - summary="Start instance", - description="Start a tool instance using Docker Compose.", -) -async def start_instance( - project_id: uuid.UUID, - repo_id: uuid.UUID, - instance_id: uuid.UUID, - data: StartInstanceRequest | None = None, - user_id: uuid.UUID = Depends(get_current_user_id), - session: AsyncSession = Depends(get_db_session), -) -> dict: - _user = await _get_user(session, user_id) - _project = await _get_owned_project(project_id, user_id, session) - try: - return await start_tool_instance( - session, user_id, project_id, repo_id, instance_id, data - ) - except ValueError as exc: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) - except RuntimeError as exc: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc) - ) - - -@router.post( - "/{project_id}/repositories/{repo_id}/instances/{instance_id}/stop", - summary="Stop instance", - description="Stop a running tool instance.", -) -async def stop_instance( - project_id: uuid.UUID, - repo_id: uuid.UUID, - instance_id: uuid.UUID, - user_id: uuid.UUID = Depends(get_current_user_id), - session: AsyncSession = Depends(get_db_session), -) -> dict: - _user = await _get_user(session, user_id) - _project = await _get_owned_project(project_id, user_id, session) - try: - return await stop_tool_instance(session, user_id, project_id, repo_id, instance_id) - except ValueError as exc: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) - - -@router.post( - "/{project_id}/repositories/{repo_id}/instances/{instance_id}/restart", - summary="Restart instance", - description="Restart a tool instance.", -) -async def restart_instance( - project_id: uuid.UUID, - repo_id: uuid.UUID, - instance_id: uuid.UUID, - user_id: uuid.UUID = Depends(get_current_user_id), - session: AsyncSession = Depends(get_db_session), -) -> dict: - _user = await _get_user(session, user_id) - _project = await _get_owned_project(project_id, user_id, session) - try: - return await restart_tool_instance( - session, user_id, project_id, repo_id, instance_id - ) - except ValueError as exc: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) - except RuntimeError as exc: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc) - ) - - -@router.delete( - "/{project_id}/repositories/{repo_id}/instances/{instance_id}", - summary="Delete instance", - description="Delete a tool instance and remove its Docker containers and files.", -) -async def delete_instance( - project_id: uuid.UUID, - repo_id: uuid.UUID, - instance_id: uuid.UUID, - force: bool = False, - user_id: uuid.UUID = Depends(get_current_user_id), - session: AsyncSession = Depends(get_db_session), -) -> None: - _user = await _get_user(session, user_id) - _project = await _get_owned_project(project_id, user_id, session) - try: - await delete_tool_instance( - session, user_id, project_id, repo_id, instance_id, force - ) - except ValueError as exc: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) - except RuntimeError as exc: - detail = str(exc) - if "uncommitted changes" in detail.lower(): - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail={ - "message": "Repository has uncommitted changes", - "changed_files": detail, - "force_required": True, - }, - ) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=detail - ) - - @router.get( "/{project_id}/repositories/{repo_id}/instances/{instance_id}/logs", summary="Get instance logs", diff --git a/apps/api/src/api/tool/tool_lifecycle.py b/apps/api/src/api/tool/tool_lifecycle.py new file mode 100644 index 0000000..d320e02 --- /dev/null +++ b/apps/api/src/api/tool/tool_lifecycle.py @@ -0,0 +1,135 @@ +"""Tool instance lifecycle API endpoints.""" + +import logging +import uuid + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from src.auth.dependencies import ( + _get_owned_project, + _get_user, + get_current_user_id, + get_db_session, +) +from src.schemas.tool import StartInstanceRequest +from src.services.tool.instance_service import ( + delete_tool_instance, + restart_tool_instance, + start_tool_instance, + stop_tool_instance, +) + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/projects", tags=["tool-instances"]) + + +@router.post( + "/{project_id}/repositories/{repo_id}/instances/{instance_id}/start", + summary="Start instance", + description="Start a tool instance using Docker Compose.", +) +async def start_instance( + project_id: uuid.UUID, + repo_id: uuid.UUID, + instance_id: uuid.UUID, + data: StartInstanceRequest | None = None, + user_id: uuid.UUID = Depends(get_current_user_id), + session: AsyncSession = Depends(get_db_session), +) -> dict: + _user = await _get_user(session, user_id) + _project = await _get_owned_project(project_id, user_id, session) + try: + return await start_tool_instance( + session, user_id, project_id, repo_id, instance_id, data + ) + except ValueError as exc: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) + except RuntimeError as exc: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc) + ) + + +@router.post( + "/{project_id}/repositories/{repo_id}/instances/{instance_id}/stop", + summary="Stop instance", + description="Stop a running tool instance.", +) +async def stop_instance( + project_id: uuid.UUID, + repo_id: uuid.UUID, + instance_id: uuid.UUID, + user_id: uuid.UUID = Depends(get_current_user_id), + session: AsyncSession = Depends(get_db_session), +) -> dict: + _user = await _get_user(session, user_id) + _project = await _get_owned_project(project_id, user_id, session) + try: + return await stop_tool_instance(session, user_id, project_id, repo_id, instance_id) + except ValueError as exc: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) + + +@router.post( + "/{project_id}/repositories/{repo_id}/instances/{instance_id}/restart", + summary="Restart instance", + description="Restart a tool instance.", +) +async def restart_instance( + project_id: uuid.UUID, + repo_id: uuid.UUID, + instance_id: uuid.UUID, + user_id: uuid.UUID = Depends(get_current_user_id), + session: AsyncSession = Depends(get_db_session), +) -> dict: + _user = await _get_user(session, user_id) + _project = await _get_owned_project(project_id, user_id, session) + try: + return await restart_tool_instance( + session, user_id, project_id, repo_id, instance_id + ) + except ValueError as exc: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) + except RuntimeError as exc: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc) + ) + + +@router.delete( + "/{project_id}/repositories/{repo_id}/instances/{instance_id}", + summary="Delete instance", + description="Delete a tool instance and remove its Docker containers and files.", +) +async def delete_instance( + project_id: uuid.UUID, + repo_id: uuid.UUID, + instance_id: uuid.UUID, + force: bool = False, + user_id: uuid.UUID = Depends(get_current_user_id), + session: AsyncSession = Depends(get_db_session), +) -> None: + _user = await _get_user(session, user_id) + _project = await _get_owned_project(project_id, user_id, session) + try: + await delete_tool_instance( + session, user_id, project_id, repo_id, instance_id, force + ) + except ValueError as exc: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) + except RuntimeError as exc: + detail = str(exc) + if "uncommitted changes" in detail.lower(): + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail={ + "message": "Repository has uncommitted changes", + "changed_files": detail, + "force_required": True, + }, + ) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=detail + ) diff --git a/apps/api/src/main.py b/apps/api/src/main.py index 2e6a67e..c8aa5e1 100644 --- a/apps/api/src/main.py +++ b/apps/api/src/main.py @@ -21,6 +21,7 @@ from src.api.tool import ( sessions_router, tool_definitions_router, tool_instances_router, + tool_lifecycle_router, tool_types_router, ) from src.api.user import auth_router, ssh_keys_router, users_router @@ -165,6 +166,7 @@ app.include_router(tool_types_router) app.include_router(tool_definitions_router) app.include_router(config_profiles_router) app.include_router(tool_instances_router) +app.include_router(tool_lifecycle_router) app.include_router(sessions_router) app.include_router(instance_proxy_router) app.include_router(terminal_router) diff --git a/apps/web/.pi-map.index.md b/apps/web/.pi-map.index.md index 5595ada..44a1bb2 100644 --- a/apps/web/.pi-map.index.md +++ b/apps/web/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/web ## role -Frontend web application for a React-based developer workspace with code editing, terminal, and routing capabilities. +Frontend web application providing a React-based UI with code editing, terminal, and routing capabilities for the "headquarter" project. ## parent index: apps/.pi-map.index.md map: apps/.pi-map.md diff --git a/apps/web/.pi-map.md b/apps/web/.pi-map.md index 8ecfdd8..49719b1 100644 --- a/apps/web/.pi-map.md +++ b/apps/web/.pi-map.md @@ -4,7 +4,7 @@ dir: apps/web index: apps/web/.pi-map.index.md ## role -Frontend web application for a React-based developer workspace with code editing, terminal, and routing capabilities. +Frontend web application providing a React-based UI with code editing, terminal, and routing capabilities for the "headquarter" project. ## files - .env.example | Template file defining example environment variables for frontend API and application URL configuration - .eslintrc.cjs | Configures ESLint for a TypeScript browser project with modern ECMAScript module support | dep: @typescript-eslint/parser, @typescript-eslint/eslint-plugin, eslint @@ -16,7 +16,7 @@ Frontend web application for a React-based developer workspace with code editing - tsconfig.json | TypeScript configuration file for a React project using Vite with modern ES2020 target and bundler module resolution | dep: typescript, react, vite - vite.config.ts | Configures Vite build tool for a React project with custom dev server port and Vitest test settings. | dep: vite, @vitejs/plugin-react ## arch -Modern React SPA built with Vite/TypeScript, served by nginx in a Docker container, using ES modules with Vitest for testing and ESLint for code quality. +Modern React SPA built with Vite and TypeScript, containerized via multi-stage Docker with nginx serving, featuring standard tooling (ESLint, Vitest) and client-side routing. ## tags react, eslint, vite, typescript, dom, application, nginx, web ## symbols diff --git a/apps/web/src/.pi-map.index.md b/apps/web/src/.pi-map.index.md index a271abc..4ebffb3 100644 --- a/apps/web/src/.pi-map.index.md +++ b/apps/web/src/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/web/src ## role -Frontend web application entry point and core infrastructure for a React-based project management or development platform. +Provides the core web application entry point, routing infrastructure, and shared domain type definitions for a React-based frontend. ## parent index: apps/web/.pi-map.index.md map: apps/web/.pi-map.md diff --git a/apps/web/src/.pi-map.md b/apps/web/src/.pi-map.md index 2e1753a..264a974 100644 --- a/apps/web/src/.pi-map.md +++ b/apps/web/src/.pi-map.md @@ -4,13 +4,13 @@ dir: apps/web/src index: apps/web/src/.pi-map.index.md ## role -Frontend web application entry point and core infrastructure for a React-based project management or development platform. +Provides the core web application entry point, routing infrastructure, and shared domain type definitions for a React-based frontend. ## files - main.tsx | Bootstraps a React application with routing, authentication, and session management providers. | dep: react, react-dom/client, react-router-dom, ./router, ./state/auth, ./state/sessions, ./styles/tokens.css, ./styles/global.css, ./styles/utilities.css, ./styles/syntax-highlight.css, ./styles/pages/git-history.css, ./styles/pages/projects.css, ./styles/pages/sessions.css, ./styles/pages/ssh-keys.css, ./styles/pages/workspace-detail.css, ./styles/pages/workspaces.css, react-dom - router.tsx | Defines the React Router configuration for a web application with protected routes, nested layouts, and redirects. | exp: AppRouter | dep: react-router-dom, ./components/app-shell, ./components/protected-route, ./pages/DashboardPage, ./pages/PlaceholderPage, ./pages/ProfilePage, ./pages/ProjectsPage, ./pages/GitRepositoriesPage, ./pages/GitHistoryPage, ./pages/ProjectSettingsPage, ./pages/SettingsPage, ./pages/TerminalPage, ./pages/ToolWorkshopPage, ./pages/SshKeysPage, ./pages/ConfigProfilesPage, ./pages/SessionsPage, ./pages/WorkspacesPage, ./pages/WorkspaceDetailPage - types.ts | Defines TypeScript type definitions for user sessions, projects, repositories, and workspaces in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos ## arch -Layered React SPA architecture with provider-based dependency injection, declarative routing with nested layouts and route guards, and centralized TypeScript type definitions for domain entities. +Modular React SPA using React Router v6 with nested route layouts, protected route guards via authentication context providers, and centralized TypeScript type definitions for cross-cutting domain models. ## tags pages, styles, css, router, react, session, dom, project ## symbols diff --git a/apps/web/src/components/.pi-map.index.md b/apps/web/src/components/.pi-map.index.md index 98692b0..4203844 100644 --- a/apps/web/src/components/.pi-map.index.md +++ b/apps/web/src/components/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/web/src/components ## role -Provides reusable, foundational React UI components for layout, authentication, data display, icons, code editing, and notifications across the web application. +Provides shared, reusable React UI components for the web application including layout shell, navigation, authentication guards, data display states, icons, code editing, syntax highlighting, and toast notifications. ## parent index: apps/web/src/.pi-map.index.md map: apps/web/src/.pi-map.md diff --git a/apps/web/src/components/.pi-map.md b/apps/web/src/components/.pi-map.md index b5ae990..89b07b2 100644 --- a/apps/web/src/components/.pi-map.md +++ b/apps/web/src/components/.pi-map.md @@ -4,7 +4,7 @@ dir: apps/web/src/components index: apps/web/src/components/.pi-map.index.md ## role -Provides reusable, foundational React UI components for layout, authentication, data display, icons, code editing, and notifications across the web application. +Provides shared, reusable React UI components for the web application including layout shell, navigation, authentication guards, data display states, icons, code editing, syntax highlighting, and toast notifications. ## files - app-shell.tsx | Renders the main application shell layout with navigation, session management, and responsive mobile/desktop views for a React Router-based app. | exp: AppShell | dep: react-router-dom, ../api/sessions, ../hooks/use-theme, ../state/auth, ../state/sessions, ../hooks/use-mobile-viewport, ../state/events, ../state/toast, ../state/notifications, ../state/session-operations, ./features/notification/event-toast-bridge, ./features/notification/notification-center, ./features/session/session-progress-panel, ./icon, ./features/mobile/mobile-nav, ./features/tool/start-tool-fab, ../utils/icons - code-editor.tsx | A React component that renders a syntax-highlighted code editor with line numbers using react-simple-code-editor. | exp: CodeEditor | dep: react, react-simple-code-editor, ../utils/language @@ -16,7 +16,7 @@ Provides reusable, foundational React UI components for layout, authentication, - toast-rules.test.ts | Unit tests for mapping instance events to toast notification categories and severities | dep: vitest, ./toast-rules, ../types/events - toast-rules.ts | Maps instance events to toast notifications with deduplication logic to prevent spam | exp: func:mapEventToCategory(event: InstanceEventPayload) → string, call:event.event.startsWith, func:mapEventToSeverity(event: InstanceEventPayload) → "info" | "warning" | "error" | "success", func:handleEventToast(event: InstanceEventPayload) → void, call:shouldShowToast, call:toast.info, call:toast.success, call:toast.warning, call:toast.error, func:clearToastDedup() → void, call:lastToastTime.clear | dep: ../state/toast, ../types/events, toast state module, InstanceEventPayload type ## arch -Modular component architecture with separation of concerns—each component handles a single responsibility, with dedicated test files alongside implementation, and higher-level components (app-shell, protected-route) composing lower-level primitives (icon, data-states, toast-rules). +Component-based architecture with clear separation of concerns: layout/navigation (app-shell), authentication (protected-route), data presentation (data-states, syntax-highlighter, code-editor), utilities (icon, toast-rules), and co-located test files following a feature-organized structure with semantic naming conventions. ## tags toast, state, react, icon, code, event, editor, protected ## symbols diff --git a/apps/web/src/components/features/.pi-map.index.md b/apps/web/src/components/features/.pi-map.index.md index 9e5e30e..1cf9906 100644 --- a/apps/web/src/components/features/.pi-map.index.md +++ b/apps/web/src/components/features/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/web/src/components/features ## role -Contains reusable React components that implement specific user-facing functionality and business logic features across the web application. +Contains reusable UI components organized by feature domains for the web application. ## parent index: apps/web/src/components/.pi-map.index.md map: apps/web/src/components/.pi-map.md diff --git a/apps/web/src/components/features/.pi-map.md b/apps/web/src/components/features/.pi-map.md index 42d5214..c46bd54 100644 --- a/apps/web/src/components/features/.pi-map.md +++ b/apps/web/src/components/features/.pi-map.md @@ -4,10 +4,10 @@ dir: apps/web/src/components/features index: apps/web/src/components/features/.pi-map.index.md ## role -Contains reusable React components that implement specific user-facing functionality and business logic features across the web application. +Contains reusable UI components organized by feature domains for the web application. ## files ## arch -Feature-based component organization with domain-specific grouping, likely using composition patterns and co-located feature logic (hooks, utils, sub-components) following a modular frontend architecture. +Feature-based colocation pattern where components are grouped by business domain rather than technical type, enabling scalable modular development with clear separation of concerns. ## tags - ## symbols diff --git a/apps/web/src/components/features/project/ProjectCard.test.tsx b/apps/web/src/components/features/project/ProjectCard.test.tsx index df42ce0..3ea4491 100644 --- a/apps/web/src/components/features/project/ProjectCard.test.tsx +++ b/apps/web/src/components/features/project/ProjectCard.test.tsx @@ -4,114 +4,114 @@ import { MemoryRouter } from "react-router-dom"; import { afterEach, describe, expect, it, vi } from "vitest"; afterEach(() => { - cleanup(); + cleanup(); }); import { ProjectCard } from "./ProjectCard"; import type { ProjectWithRepos } from "../../../types"; const mockProject: ProjectWithRepos = { - id: "proj-1", - name: "Alpha Project", - description: "First project", - owner_id: "user-1", - default_ssh_key_id: null, - created_at: "2026-06-01T00:00:00Z", - repositories: [ - { - id: "repo-1", - name: "my-repo", - remote_url: "https://example.com/repo.git", - workspaces: [ - { - id: "ws-1", - name: "dev", - branch: "main", - status: "ready", - instance_count: 2, - }, - ], - }, - ], + id: "proj-1", + name: "Alpha Project", + description: "First project", + owner_id: "user-1", + default_ssh_key_id: null, + created_at: "2026-06-01T00:00:00Z", + repositories: [ + { + id: "repo-1", + name: "my-repo", + remote_url: "https://example.com/repo.git", + workspaces: [ + { + id: "ws-1", + name: "dev", + branch: "main", + status: "ready", + instance_count: 2, + }, + ], + }, + ], }; describe("ProjectCard", () => { - it("renders project name and repository", () => { - render( - - - - ); + it("renders project name and repository", () => { + render( + + + , + ); - expect(screen.getByText("Alpha Project")).toBeInTheDocument(); - expect(screen.getByText("my-repo")).toBeInTheDocument(); - expect(screen.getByText("dev")).toBeInTheDocument(); - }); + expect(screen.getByText("Alpha Project")).toBeInTheDocument(); + expect(screen.getByText("my-repo")).toBeInTheDocument(); + expect(screen.getByText("dev")).toBeInTheDocument(); + }); - it("calls onCreateWorkspace when new workspace button is clicked", () => { - const onCreateWorkspace = vi.fn(); - render( - - - - ); + it("calls onCreateWorkspace when new workspace button is clicked", () => { + const onCreateWorkspace = vi.fn(); + render( + + + , + ); - fireEvent.click(screen.getByRole("button", { name: /new workspace/i })); - expect(onCreateWorkspace).toHaveBeenCalledWith("repo-1"); - }); + fireEvent.click(screen.getByRole("button", { name: /new workspace/i })); + expect(onCreateWorkspace).toHaveBeenCalledWith("repo-1"); + }); - it("shows delete confirmation", () => { - render( - - - - ); + it("shows delete confirmation", () => { + render( + + + , + ); - expect(screen.getByText(/are you sure/i)).toBeInTheDocument(); - }); + expect(screen.getByText(/are you sure/i)).toBeInTheDocument(); + }); }); diff --git a/apps/web/src/components/features/workspace/.pi-map.index.md b/apps/web/src/components/features/workspace/.pi-map.index.md index ba52a36..6bfb146 100644 --- a/apps/web/src/components/features/workspace/.pi-map.index.md +++ b/apps/web/src/components/features/workspace/.pi-map.index.md @@ -2,24 +2,28 @@ dir: apps/web/src/components/features/workspace ## role -Provides the workspace UI shell and container components that orchestrate the repository browsing, editing, and tool execution environment for the web application. +Provides UI components for workspace management, navigation, and detail views in the web application. ## parent index: apps/web/src/components/features/.pi-map.index.md map: apps/web/src/components/features/.pi-map.md ## children - ## files -- FileBrowser.tsx -- WorkspaceLayout.tsx - workspace-card.tsx - workspace-create-form.tsx +- workspace-detail-header.tsx +- workspace-file-panel.tsx +- workspace-git-panel.tsx - workspace-header.tsx - workspace-instance-chips.tsx +- workspace-settings-panel.tsx +- workspace-tab-bar.tsx +- workspace-tools-panel.tsx ## links index: apps/web/src/components/features/workspace/.pi-map.index.md map: apps/web/src/components/features/workspace/.pi-map.md ## workflows - change workspace behavior - read: FileBrowser.tsx, WorkspaceLayout.tsx, workspace-card.tsx + read: workspace-card.tsx, workspace-create-form.tsx, workspace-detail-header.tsx ## dirty - diff --git a/apps/web/src/components/features/workspace/.pi-map.md b/apps/web/src/components/features/workspace/.pi-map.md index 77e6518..ac3e0e6 100644 --- a/apps/web/src/components/features/workspace/.pi-map.md +++ b/apps/web/src/components/features/workspace/.pi-map.md @@ -4,29 +4,33 @@ dir: apps/web/src/components/features/workspace index: apps/web/src/components/features/workspace/.pi-map.index.md ## role -Provides the workspace UI shell and container components that orchestrate the repository browsing, editing, and tool execution environment for the web application. +Provides UI components for workspace management, navigation, and detail views in the web application. ## files -- FileBrowser.tsx | A React component that displays a browsable file tree for a git repository with directory navigation, file status indicators, and URL-based state management. | exp: FileBrowser | dep: react, react-router-dom, ../../../api/client, ../../data-states, ../../icon, ../../../api/git-repositories, apiClient, EmptyState, Icon -- WorkspaceLayout.tsx | Renders a responsive workspace layout that switches between mobile tab-based navigation and desktop sidebar/main panel layout for repository file browsing, editing, git operations, and terminal tools. | exp: WorkspaceLayout | dep: ../../icon, ./FileBrowser, ../git/file-editor, ../git/commit-panel, ../git/git-toolbar, ../tool/instance-list, ../../../api/git-repositories, ../../../api/tool-types, ../../../hooks/use-repo-workspace, Icon, FileBrowser, FileEditor, CommitPanel, GitToolbar, InstanceList, GitRepository, GitStatus, ToolType, Project - workspace-card.tsx | React card component that displays workspace information with status, metadata, and action buttons | exp: WorkspaceCardProps, func:WorkspaceCard({ workspace, loading = false, onStartTool, onSync, onDelete, }: WorkspaceCardProps), call:onStartTool, call:onSync, call:onDelete | dep: react-router-dom, ../../icon, ./workspace-instance-chips, ../../../types/workspace - workspace-create-form.tsx | React form component for creating workspaces with cascading project/repository/branch selectors and support for both contextual and standalone modes | exp: WorkspaceCreateFormProps, func:WorkspaceCreateForm({ onSubmit, onCancel, defaultProjectId, defaultRepoId, }: WorkspaceCreateFormProps), call:Boolean, call:useState, call:useGitRepo, call:useEffect, call:git.branches.includes, call:setSelectedBranch, call:setIsNewBranch, call:useCallback, call:listProjects, call:setProjects, call:setSelectedProject, call:setError, call:setFetchingProjects, call:loadProjects, call:setRepos, call:setSelectedRepo, call:listRepositories, call:loadRepos, call:setNewBranchName, call:e.preventDefault, call:name.trim, call:newBranchName.trim, call:setSubmitting, call:createWorkspaceTopLevel, call:onSubmit, call:projects.map, call:repos.map, call:setName, call:handleBranchChange, call:git.branches.map | dep: react, ../../icon, ../../../api/projects, ../../../api/git-repositories, ../../../api/workspaces, ../../../hooks/use-git-repo, ../../../types, icon, api/projects, api/git-repositories, api/workspaces, hooks/use-git-repo, types +- workspace-detail-header.tsx | Renders a header component for a workspace detail page displaying breadcrumb navigation and branch information. | exp: WorkspaceDetailHeaderProps, func:WorkspaceDetailHeader({ workspace }: WorkspaceDetailHeaderProps) | dep: ../../icon, icon +- workspace-file-panel.tsx | Renders a file browser and editor panel with Git integration for a workspace detail page. | exp: func:WorkspaceFilePanel({ workspaceId }: WorkspaceFilePanelProps), call:useWorkspaceFiles, call:useWorkspaceGit, call:useState, call:setSelectedPath, call:setIsEditing, call:setEditContent, call:navigateTo, call:loadFile, call:currentPath.split("/").slice(0, -1).join, call:saveFile, call:setCommitMessage, call:commit, call:entries.map, call:handleSelect | dep: react, ../../icon, ../../../hooks/use-workspace-files, ../../../hooks/use-workspace-git, ../../../api/workspace-files, icon, use-workspace-files, use-workspace-git, workspace-files API types +- workspace-git-panel.tsx | Renders a Git panel for a workspace detail page that displays branch selection and commit history. | exp: func:WorkspaceGitPanel({ workspaceId }: WorkspaceGitPanelProps), call:useWorkspaceGit, call:checkout, call:branches.map, call:history.map, call:commit.hash.slice | dep: ../../../hooks/use-workspace-git, React, useWorkspaceGit hook - workspace-header.tsx | Renders a workspace header component displaying project info with navigation links to history and settings pages. | exp: WorkspaceHeader | dep: react-router-dom, ../../icon, icon - workspace-instance-chips.tsx | Displays running tool instances for a workspace as clickable status chips with external links. | exp: func:WorkspaceInstanceChips({ workspaceId, }: WorkspaceInstanceChipsProps), call:useState, call:useEffect, call:listWorkspaceInstances, call:setInstances, call:setLoading, call:load, call:instances.map, call:e.stopPropagation | dep: react, ../../../api/workspace-instances, ../../../api/sessions +- workspace-settings-panel.tsx | Displays a read-only settings panel showing workspace metadata on a workspace detail page. | exp: func:WorkspaceSettingsPanel({ workspace }: WorkspaceSettingsPanelProps) | dep: ../../../types/workspace, React, types/workspace +- workspace-tab-bar.tsx | Renders a tab bar component for workspace navigation with desktop and mobile variants. | exp: WorkspaceTab, func:WorkspaceTabBar({ active, onChange }: WorkspaceTabBarProps), call:TABS.map, call:onChange, func:WorkspaceMobileTabBar({ active, onChange }: WorkspaceTabBarProps), call:TABS.map, call:onChange | dep: ../../icon, icon +- workspace-tools-panel.tsx | Displays and manages running tool instances for a workspace with ability to start new tools via modal | exp: func:WorkspaceToolsPanel({ workspace }: WorkspaceToolsPanelProps), call:useWorkspaceInstances, call:useState, call:setShowModal, call:instances.map, call:e.stopPropagation, call:refresh | dep: react, ../../icon, ../tool/tool-starter, ../../../hooks/use-workspace-instances, ../../../types/workspace, icon, tool-starter, use-workspace-instances ## arch -Compound component architecture with responsive layout switching (mobile/desktop), URL-driven state management for file navigation, and feature-based composition combining card/list views, forms, headers, and dynamic tool instance indicators. +Feature-based component organization with page-specific composite components (header/panel/tab-bar) and atomic display components, following a compound component pattern for workspace detail page layout. ## tags -workspace, call:set, git, api, call:use, branch, projects, react +workspace, call:set, call:use, git, panel, icon, branch, call:on ## symbols - WorkspaceCard - WorkspaceCreateForm +- WorkspaceDetailHeader +- WorkspaceFilePanel +- WorkspaceGitPanel - WorkspaceInstanceChips -- FileBrowser -- WorkspaceLayout -- WorkspaceCardProps -- call:onStartTool -- call:onSync +- WorkspaceSettingsPanel +- WorkspaceTabBar ## workflows - change workspace behavior - read: FileBrowser.tsx, WorkspaceLayout.tsx, workspace-card.tsx + read: workspace-card.tsx, workspace-create-form.tsx, workspace-detail-header.tsx ## dirty - diff --git a/apps/web/src/components/features/workspace/workspace-detail-header.tsx b/apps/web/src/components/features/workspace/workspace-detail-header.tsx new file mode 100644 index 0000000..512c3e9 --- /dev/null +++ b/apps/web/src/components/features/workspace/workspace-detail-header.tsx @@ -0,0 +1,31 @@ +/** Header for the workspace detail page. */ + +import { Icon } from "../../icon"; + +export interface WorkspaceDetailHeaderProps { + workspace: { + name: string; + repo_name: string; + project_name: string; + branch: string; + }; +} + +export function WorkspaceDetailHeader({ workspace }: WorkspaceDetailHeaderProps) { + return ( +
+
+ {workspace.project_name} + / + {workspace.repo_name} + / + {workspace.name} +
+
+ + {workspace.branch} + +
+
+ ); +} diff --git a/apps/web/src/components/features/workspace/workspace-file-panel.tsx b/apps/web/src/components/features/workspace/workspace-file-panel.tsx new file mode 100644 index 0000000..f4626fc --- /dev/null +++ b/apps/web/src/components/features/workspace/workspace-file-panel.tsx @@ -0,0 +1,181 @@ +/** Files tab for the workspace detail page. */ + +import { useState } from "react"; +import { Icon } from "../../icon"; +import { useWorkspaceFiles } from "../../../hooks/use-workspace-files"; +import { useWorkspaceGit } from "../../../hooks/use-workspace-git"; +import type { FileEntry } from "../../../api/workspace-files"; + +interface WorkspaceFilePanelProps { + workspaceId: string; +} + +export function WorkspaceFilePanel({ workspaceId }: WorkspaceFilePanelProps) { + const { + entries, + content, + currentPath, + loadFile, + saveFile, + loading, + error, + navigateTo, + } = useWorkspaceFiles(workspaceId); + const { status, commit, push, pull, fetch } = useWorkspaceGit(workspaceId); + const [selectedPath, setSelectedPath] = useState(null); + const [editContent, setEditContent] = useState(null); + const [isEditing, setIsEditing] = useState(false); + const [commitMessage, setCommitMessage] = useState(""); + + const handleSelect = (entry: FileEntry) => { + if (entry.type === "directory") { + setSelectedPath(null); + setIsEditing(false); + setEditContent(null); + navigateTo(entry.path); + return; + } + setSelectedPath(entry.path); + setIsEditing(false); + setEditContent(null); + loadFile(entry.path); + }; + + const navigateUp = () => { + if (!currentPath) return; + const parentPath = currentPath.split("/").slice(0, -1).join("/"); + navigateTo(parentPath); + setSelectedPath(null); + setIsEditing(false); + setEditContent(null); + }; + + const handleEdit = () => { + if (content !== null) { + setEditContent(content); + setIsEditing(true); + } + }; + + const handleSave = async () => { + if (selectedPath && editContent !== null) { + await saveFile(selectedPath, editContent, commitMessage || undefined); + setIsEditing(false); + setCommitMessage(""); + } + }; + + return ( +
+ {status && ( +
+
+ {status.modified.length > 0 && ( + + M {status.modified.length} + + )} + {status.added.length > 0 && ( + A {status.added.length} + )} + {status.deleted.length > 0 && ( + D {status.deleted.length} + )} + {status.untracked.length > 0 && ( + + ? {status.untracked.length} + + )} +
+
+ setCommitMessage(e.target.value)} + placeholder="Commit message" + /> + + + + +
+
+ )} +
+
+ {currentPath && ( + + )} + {loading &&

Loading...

} + {error &&

{error}

} + {entries.map((entry) => ( + + ))} +
+
+ {selectedPath ? ( + <> +
+ {selectedPath} + {!isEditing && ( + + )} +
+ {isEditing ? ( + <> +