Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 346b32236a |
+1
-1
@@ -16,7 +16,7 @@ dir: .
|
|||||||
Trust boundary: index routes, map orients, source decides.
|
Trust boundary: index routes, map orients, source decides.
|
||||||
|
|
||||||
## role
|
## role
|
||||||
A self-hosted web-based project management and Git repository platform with OAuth2 authentication, designed for deployment via Docker Compose with Traefik reverse proxy.
|
Infrastructure and deployment configuration package for a self-hosted project management platform with OAuth2 authentication, providing containerized orchestration, environment templates, and development tooling.
|
||||||
## parent
|
## parent
|
||||||
-
|
-
|
||||||
## children
|
## children
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@ index: ./.pi-map.index.md
|
|||||||
Trust boundary: index routes, map orients, source decides.
|
Trust boundary: index routes, map orients, source decides.
|
||||||
|
|
||||||
## role
|
## role
|
||||||
A self-hosted web-based project management and Git repository platform with OAuth2/SSO authentication, containerized for Docker deployment.
|
Infrastructure and deployment configuration package for a self-hosted project management platform with OAuth2 authentication, providing containerized orchestration, environment templates, and development tooling.
|
||||||
## files
|
## files
|
||||||
- .env.example | Provides a template of environment variables for configuring a Headquarter application with PostgreSQL, Redis, Authentik SSO, and Docker/Traefik deployment
|
- .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
|
- .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 @@ A self-hosted web-based project management and Git repository platform with OAut
|
|||||||
- progress.md | Tracks completed and remaining tasks for a backend-frontend code refactoring project organized in 7 phases
|
- 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
|
- swap-pane | Empty file with no functionality
|
||||||
## arch
|
## arch
|
||||||
Multi-service containerized architecture (PostgreSQL, Redis, API backend, web frontend) with reverse proxy integration (Traefik), environment-driven configuration, and AI-agent-guided development workflows via OpenSpec.
|
Docker Compose-based microservices architecture with frontend/backend separation, PostgreSQL/Redis data layer, Traefik reverse proxy integration, and environment-driven configuration management following twelve-factor app principles.
|
||||||
## tags
|
## tags
|
||||||
docker, redis, git, application, postgresql, compose, traefik, project
|
docker, redis, git, application, postgresql, compose, traefik, project
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps
|
dir: apps
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains the main application entry points and top-level configurations for different deployable targets.
|
Contains the main deployable application modules or entry points for the project.
|
||||||
## parent
|
## parent
|
||||||
index: ./.pi-map.index.md
|
index: ./.pi-map.index.md
|
||||||
map: ./.pi-map.md
|
map: ./.pi-map.md
|
||||||
|
|||||||
+2
-2
@@ -4,10 +4,10 @@ dir: apps
|
|||||||
index: apps/.pi-map.index.md
|
index: apps/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains the main deployable applications or entry points for the project.
|
Contains the main deployable application modules or entry points for the project.
|
||||||
## files
|
## files
|
||||||
## arch
|
## arch
|
||||||
Typically follows a multi-app workspace pattern where each subdirectory is an independent executable or service sharing common libraries.
|
Modular monolith or microservices architecture with separate application boundaries, each potentially having its own configuration, dependencies, and lifecycle.
|
||||||
## tags
|
## tags
|
||||||
-
|
-
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,23 +2,17 @@
|
|||||||
dir: apps/api
|
dir: apps/api
|
||||||
|
|
||||||
## role
|
## role
|
||||||
FastAPI backend API that manages projects, git repositories, and development tools through Docker containers with PostgreSQL persistence.
|
Self-hosted FastAPI backend API that manages projects, git repositories, and development tools via Docker instances.
|
||||||
## parent
|
## parent
|
||||||
index: apps/.pi-map.index.md
|
index: apps/.pi-map.index.md
|
||||||
map: apps/.pi-map.md
|
map: apps/.pi-map.md
|
||||||
## children
|
## children
|
||||||
- apps/api/.mypy_cache
|
|
||||||
index: apps/api/.mypy_cache/.pi-map.index.md
|
|
||||||
map: apps/api/.mypy_cache/.pi-map.md
|
|
||||||
- apps/api/.pi-lens
|
- apps/api/.pi-lens
|
||||||
index: apps/api/.pi-lens/.pi-map.index.md
|
index: apps/api/.pi-lens/.pi-map.index.md
|
||||||
map: apps/api/.pi-lens/.pi-map.md
|
map: apps/api/.pi-lens/.pi-map.md
|
||||||
- apps/api/.pytest_cache
|
- apps/api/.pytest_cache
|
||||||
index: apps/api/.pytest_cache/.pi-map.index.md
|
index: apps/api/.pytest_cache/.pi-map.index.md
|
||||||
map: apps/api/.pytest_cache/.pi-map.md
|
map: apps/api/.pytest_cache/.pi-map.md
|
||||||
- apps/api/.ruff_cache
|
|
||||||
index: apps/api/.ruff_cache/.pi-map.index.md
|
|
||||||
map: apps/api/.ruff_cache/.pi-map.md
|
|
||||||
- apps/api/.venv-test
|
- apps/api/.venv-test
|
||||||
index: apps/api/.venv-test/.pi-map.index.md
|
index: apps/api/.venv-test/.pi-map.index.md
|
||||||
map: apps/api/.venv-test/.pi-map.md
|
map: apps/api/.venv-test/.pi-map.md
|
||||||
|
|||||||
+1
-1
@@ -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
|
- 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
|
- 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
|
## arch
|
||||||
Multi-stage containerized architecture with async PostgreSQL database, Alembic migrations, Cloudflare tunneling, Docker socket access, and uv-based Python dependency management.
|
Async Python/FastAPI with PostgreSQL (Alembic migrations), multi-stage Docker deployment with Cloudflare tunneling, uv package management, and containerized service orchestration.
|
||||||
## tags
|
## tags
|
||||||
docker, alembic, python, database, fastapi, postgresql, asyncpg, uvicorn
|
docker, alembic, python, database, fastapi, postgresql, asyncpg, uvicorn
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/alembic
|
dir: apps/api/alembic
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Database migration infrastructure for the API application, enabling version-controlled schema changes with async SQLAlchemy support.
|
Database migration infrastructure for the API application, providing version-controlled schema evolution with async SQLAlchemy support.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/.pi-map.index.md
|
index: apps/api/.pi-map.index.md
|
||||||
map: apps/api/.pi-map.md
|
map: apps/api/.pi-map.md
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ dir: apps/api/alembic
|
|||||||
index: apps/api/alembic/.pi-map.index.md
|
index: apps/api/alembic/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Database migration infrastructure for the API application, enabling version-controlled schema changes with async SQLAlchemy support.
|
Database migration infrastructure for the API application, providing version-controlled schema evolution with async SQLAlchemy support.
|
||||||
## files
|
## files
|
||||||
- env.py | Configures Alembic database migration environment with async SQLAlchemy support for a project. | exp: func:run_migrations_offline() → None, call:context.configure, call:context.begin_transaction, call:context.run_migrations, func:do_run_migrations(connection: Connection) → None, call:context.configure, call:context.begin_transaction, call:context.run_migrations, func:run_async_migrations() → None, call:async_engine_from_config, call:config.get_section, call:connectable.connect, call:connection.run_sync, call:connectable.dispose, func:run_migrations_online() → None, call:asyncio.run, call:run_async_migrations | dep: logging.config, alembic, sqlalchemy, sqlalchemy.engine, sqlalchemy.ext.asyncio, src.config, src.models, asyncio
|
- env.py | Configures Alembic database migration environment with async SQLAlchemy support for a project. | exp: func:run_migrations_offline() → None, call:context.configure, call:context.begin_transaction, call:context.run_migrations, func:do_run_migrations(connection: Connection) → None, call:context.configure, call:context.begin_transaction, call:context.run_migrations, func:run_async_migrations() → None, call:async_engine_from_config, call:config.get_section, call:connectable.connect, call:connection.run_sync, call:connectable.dispose, func:run_migrations_online() → None, call:asyncio.run, call:run_async_migrations | dep: logging.config, alembic, sqlalchemy, sqlalchemy.engine, sqlalchemy.ext.asyncio, src.config, src.models, asyncio
|
||||||
- script.py.mako | Alembic database migration script template that generates upgrade/downgrade functions for SQLAlchemy schema migrations | dep: alembic, sqlalchemy
|
- script.py.mako | Alembic database migration script template that generates upgrade/downgrade functions for SQLAlchemy schema migrations | dep: alembic, sqlalchemy
|
||||||
## arch
|
## arch
|
||||||
Alembic migration framework with Mako templating for script generation, async SQLAlchemy engine configuration, and environment-based database connection management.
|
Alembic migration framework with Mako templating for generating revision scripts, async SQLAlchemy engine configuration, and autogenerate capabilities for schema change tracking.
|
||||||
## tags
|
## tags
|
||||||
migrations, run, sqlalchemy, async, alembic, call:context.configure, call:context.begin, transaction
|
migrations, run, sqlalchemy, async, alembic, call:context.configure, call:context.begin, transaction
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/alembic/versions
|
dir: apps/api/alembic/versions
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Database schema evolution and versioning management for the API application using Alembic migration scripts.
|
Manages incremental database schema evolution for the API application using Alembic migrations, tracking all table creations, column additions, relationship changes, and data transformations over the project's lifecycle.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/alembic/.pi-map.index.md
|
index: apps/api/alembic/.pi-map.index.md
|
||||||
map: apps/api/alembic/.pi-map.md
|
map: apps/api/alembic/.pi-map.md
|
||||||
@@ -48,7 +48,6 @@ map: apps/api/alembic/.pi-map.md
|
|||||||
- 2026_05_29_remove_ssh_keys_mount_from_manifest.py
|
- 2026_05_29_remove_ssh_keys_mount_from_manifest.py
|
||||||
- 2026_06_01_add_workspaces.py
|
- 2026_06_01_add_workspaces.py
|
||||||
- 2026_06_13_make_clone_mode_nullable.py
|
- 2026_06_13_make_clone_mode_nullable.py
|
||||||
- 2026_06_14_104415_add_tool_type_home_directory.py
|
|
||||||
- 398082499c30_add_tool_config_fields.py
|
- 398082499c30_add_tool_config_fields.py
|
||||||
- 6fc7bfcf199f_merge_remove_is_builtin_and_add_config_.py
|
- 6fc7bfcf199f_merge_remove_is_builtin_and_add_config_.py
|
||||||
- 86cec91fdb00_merge_profile_resolver_and_workspaces_.py
|
- 86cec91fdb00_merge_profile_resolver_and_workspaces_.py
|
||||||
@@ -56,7 +55,6 @@ map: apps/api/alembic/.pi-map.md
|
|||||||
- 8ed7dd80973d_create_config_folders_table.py
|
- 8ed7dd80973d_create_config_folders_table.py
|
||||||
- af8512103d67_add_tool_type_fields.py
|
- af8512103d67_add_tool_type_fields.py
|
||||||
- f3d2dc90ba3a_merge_single_interface_and_clone_mode.py
|
- f3d2dc90ba3a_merge_single_interface_and_clone_mode.py
|
||||||
- fc8f1a20cbf6_merge_home_directory_and_pi_agent_mount_.py
|
|
||||||
## links
|
## links
|
||||||
index: apps/api/alembic/versions/.pi-map.index.md
|
index: apps/api/alembic/versions/.pi-map.index.md
|
||||||
map: apps/api/alembic/versions/.pi-map.md
|
map: apps/api/alembic/versions/.pi-map.md
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ dir: apps/api/alembic/versions
|
|||||||
index: apps/api/alembic/versions/.pi-map.index.md
|
index: apps/api/alembic/versions/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Database schema evolution and versioning management for the API application using Alembic migration scripts.
|
Manages incremental database schema evolution for the API application using Alembic migrations, tracking all table creations, column additions, relationship changes, and data transformations over the project's lifecycle.
|
||||||
## files
|
## files
|
||||||
- 0001_initial_schema.py | Defines the initial database schema migration creating five tables (users, ssh_keys, projects, git_repositories, user_configs) with relationships, indexes, and constraints using Alembic. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.String, call:postgresql.UUID, call:sa.DateTime, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:op.create_index, call:op.f, call:sa.Text, call:sa.ForeignKeyConstraint, call:sa.Boolean, call:postgresql.JSONB, func:downgrade() → None, call:op.drop_table, call:op.drop_index, call:op.f | dep: alembic, sqlalchemy.dialects, sqlalchemy, postgresql dialect
|
- 0001_initial_schema.py | Defines the initial database schema migration creating five tables (users, ssh_keys, projects, git_repositories, user_configs) with relationships, indexes, and constraints using Alembic. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.String, call:postgresql.UUID, call:sa.DateTime, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:op.create_index, call:op.f, call:sa.Text, call:sa.ForeignKeyConstraint, call:sa.Boolean, call:postgresql.JSONB, func:downgrade() → None, call:op.drop_table, call:op.drop_index, call:op.f | dep: alembic, sqlalchemy.dialects, sqlalchemy, postgresql dialect
|
||||||
- 0002_refresh_tokens.py | Alembic database migration that creates a refresh_tokens table with indexes for user authentication token management | exp: func:upgrade() → None, call:op.get_bind, call:sa.inspect, call:inspector.has_table, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.String, call:sa.DateTime, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:inspector.get_indexes, call:op.f, call:op.create_index, func:downgrade() → None, call:op.get_bind, call:sa.inspect, call:inspector.has_table, call:inspector.get_indexes, call:op.f, call:op.drop_index, call:op.drop_table | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
- 0002_refresh_tokens.py | Alembic database migration that creates a refresh_tokens table with indexes for user authentication token management | exp: func:upgrade() → None, call:op.get_bind, call:sa.inspect, call:inspector.has_table, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.String, call:sa.DateTime, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:inspector.get_indexes, call:op.f, call:op.create_index, func:downgrade() → None, call:op.get_bind, call:sa.inspect, call:inspector.has_table, call:inspector.get_indexes, call:op.f, call:op.drop_index, call:op.drop_table | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||||
@@ -24,7 +24,7 @@ Database schema evolution and versioning management for the API application usin
|
|||||||
- 0014_merge_heads.py | Alembic merge migration that reconciles two divergent migration branches into a single history line | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
|
- 0014_merge_heads.py | Alembic merge migration that reconciles two divergent migration branches into a single history line | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
|
||||||
- 0015_single_interface.py | Alembic database migration that replaces a JSON array `interfaces` column with `interface_type` string and `requires_port` boolean columns in the `tool_types` table, with dialect-specific data migration for PostgreSQL and SQLite. | exp: func:_get_dialect() → str, call:op.get_bind, func:upgrade() → None, call:_get_dialect, call:op.add_column, call:sa.Column, call:sa.String, call:sa.Boolean, call:op.execute, call:op.alter_column, call:op.drop_column, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:_get_dialect, call:op.drop_constraint, call:op.add_column, call:sa.Column, call:postgresql.JSONB, call:sa.Text, call:op.execute, call:sa.JSON, call:op.drop_column | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, postgresql (dialect)
|
- 0015_single_interface.py | Alembic database migration that replaces a JSON array `interfaces` column with `interface_type` string and `requires_port` boolean columns in the `tool_types` table, with dialect-specific data migration for PostgreSQL and SQLite. | exp: func:_get_dialect() → str, call:op.get_bind, func:upgrade() → None, call:_get_dialect, call:op.add_column, call:sa.Column, call:sa.String, call:sa.Boolean, call:op.execute, call:op.alter_column, call:op.drop_column, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:_get_dialect, call:op.drop_constraint, call:op.add_column, call:sa.Column, call:postgresql.JSONB, call:sa.Text, call:op.execute, call:sa.JSON, call:op.drop_column | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, postgresql (dialect)
|
||||||
- 069d3da4dc9b_add_ssh_key_id_to_config_profiles.py | Alembic database migration that adds a nullable UUID foreign key column `ssh_key_id` to the `config_profiles` table referencing `ssh_keys.id` with SET NULL on delete | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Uuid, call:sa.ForeignKey, func:downgrade() → None, call:op.drop_column | dep: alembic, sqlalchemy
|
- 069d3da4dc9b_add_ssh_key_id_to_config_profiles.py | Alembic database migration that adds a nullable UUID foreign key column `ssh_key_id` to the `config_profiles` table referencing `ssh_keys.id` with SET NULL on delete | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Uuid, call:sa.ForeignKey, func:downgrade() → None, call:op.drop_column | dep: alembic, sqlalchemy
|
||||||
- 20260527160017_add_pi_agent_tool_type.py | Alembic database migration that adds a new "pi-agent" tool type with terminal-based Docker environment for the Pi coding agent | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text("SELECT id FROM tool_types WHERE name = 'pi-agent'") ).fetchone, call:sa.text, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text | dep: json, typing, alembic, uuid, sqlalchemy
|
- 20260527160017_add_pi_agent_tool_type.py | Alembic database migration that adds a "pi-agent" terminal-based coding tool type to a tool_types table with Docker configuration templates | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text("SELECT id FROM tool_types WHERE name = 'pi-agent'") ).fetchone, call:sa.text, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text | dep: json, typing, alembic, uuid, sqlalchemy
|
||||||
- 2026_05_22_add_clone_mode.py | Alembic database migration that adds ssh_key_id foreign key to git_repositories table and clone_mode/branch columns to tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:op.create_foreign_key, call:sa.String, func:downgrade() → None, call:op.drop_column, call:op.drop_constraint | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
- 2026_05_22_add_clone_mode.py | Alembic database migration that adds ssh_key_id foreign key to git_repositories table and clone_mode/branch columns to tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:op.create_foreign_key, call:sa.String, func:downgrade() → None, call:op.drop_column, call:op.drop_constraint | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||||
- 2026_05_23_remove_is_builtin.py | Alembic database migration to remove the `is_builtin` column from the `tool_types` table | exp: func:upgrade() → None, call:op.execute, func:downgrade() → None, call:op.add_column, call:sa.Column, call:sa.Boolean | dep: alembic, sqlalchemy
|
- 2026_05_23_remove_is_builtin.py | Alembic database migration to remove the `is_builtin` column from the `tool_types` table | exp: func:upgrade() → None, call:op.execute, func:downgrade() → None, call:op.add_column, call:sa.Column, call:sa.Boolean | dep: alembic, sqlalchemy
|
||||||
- 2026_05_24_220141_add_startup_command.py | Alembic database migration that adds a nullable `startup_command` text column to the `tool_types` table. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Text, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
- 2026_05_24_220141_add_startup_command.py | Alembic database migration that adds a nullable `startup_command` text column to the `tool_types` table. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Text, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
||||||
@@ -33,7 +33,7 @@ Database schema evolution and versioning management for the API application usin
|
|||||||
- 2026_05_27_external_repos.py | Alembic database migration that makes project_id nullable in git_repositories table to support external repositories and expands alembic_version version_num column to 64 characters. | exp: func:upgrade() → None, call:op.execute, call:op.alter_column, call:sa.UUID, func:downgrade() → None, call:op.alter_column, call:sa.UUID, call:op.execute | dep: typing, alembic, sqlalchemy
|
- 2026_05_27_external_repos.py | Alembic database migration that makes project_id nullable in git_repositories table to support external repositories and expands alembic_version version_num column to 64 characters. | exp: func:upgrade() → None, call:op.execute, call:op.alter_column, call:sa.UUID, func:downgrade() → None, call:op.alter_column, call:sa.UUID, call:op.execute | dep: typing, alembic, sqlalchemy
|
||||||
- 2026_05_28_add_monitoring_tables.py | Alembic database migration that creates monitoring tables (instance_events and health_checks) with indexes for tracking tool instance events and health checks | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.DateTime, call:sa.func.now, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:sa.Boolean, call:sa.Integer, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
- 2026_05_28_add_monitoring_tables.py | Alembic database migration that creates monitoring tables (instance_events and health_checks) with indexes for tracking tool instance events and health checks | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.DateTime, call:sa.func.now, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:sa.Boolean, call:sa.Integer, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
||||||
- 2026_05_28_add_terminal_sessions_table.py | Alembic database migration that creates a terminal_sessions table with tracking columns and foreign key to tool_instances | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.DateTime, call:sa.text, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:op.f, func:downgrade() → None, call:op.drop_index, call:op.f, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
- 2026_05_28_add_terminal_sessions_table.py | Alembic database migration that creates a terminal_sessions table with tracking columns and foreign key to tool_instances | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.DateTime, call:sa.text, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:op.f, func:downgrade() → None, call:op.drop_index, call:op.f, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
||||||
- 2026_05_28_add_tool_definition_manifests.py | Alembic database migration that creates a tool_definition_manifests table, adds manifest support to existing tool_types and tool_instances tables, and seeds initial data with a base Ubuntu image and pi-agent manifest while migrating the legacy pi-agent from Dockerfile templates to the new manifest system. | exp: func:upgrade() → None, call:op.get_bind, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.Boolean, call:sa.TIMESTAMP, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:sa.ForeignKeyConstraint, call:sa.CheckConstraint, call:conn.execute, call:sa.text, call:result.fetchone, call:op.add_column, call:op.create_foreign_key, call:op.drop_constraint, call:op.execute, call:json.dumps, call:str, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:op.drop_column, call:op.drop_constraint, call:op.drop_table | dep: json, uuid, typing, alembic, sqlalchemy
|
- 2026_05_28_add_tool_definition_manifests.py | Alembic database migration that creates a tool_definition_manifests table, adds manifest-related columns to tool_types and tool_instances, and migrates the pi-agent tool from Dockerfile-based to manifest-based definitions with seed data. | exp: func:upgrade() → None, call:op.get_bind, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.Boolean, call:sa.TIMESTAMP, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:sa.ForeignKeyConstraint, call:sa.CheckConstraint, call:conn.execute, call:sa.text, call:result.fetchone, call:op.add_column, call:op.create_foreign_key, call:op.drop_constraint, call:op.execute, call:json.dumps, call:str, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:op.drop_column, call:op.drop_constraint, call:op.drop_table | dep: json, uuid, typing, alembic, sqlalchemy
|
||||||
- 2026_05_28_drop_tool_configs_and_config_folders.py | Alembic database migration that drops `tool_configs` and `config_folders` tables with conditional existence checks and full downgrade recreation | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:op.drop_table, func:downgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.Boolean, call:sa.TIMESTAMP, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.Integer | dep: typing, alembic, sqlalchemy
|
- 2026_05_28_drop_tool_configs_and_config_folders.py | Alembic database migration that drops `tool_configs` and `config_folders` tables with conditional existence checks and full downgrade recreation | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:op.drop_table, func:downgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.Boolean, call:sa.TIMESTAMP, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.Integer | dep: typing, alembic, sqlalchemy
|
||||||
- 2026_05_29_add_notifications_table.py | Alembic database migration that creates a notifications table with user-linked, categorized, severity-graded messages supporting read/dismissed tracking and optimized querying indexes. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.DateTime, call:sa.func.now, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:sa.text, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
- 2026_05_29_add_notifications_table.py | Alembic database migration that creates a notifications table with user-linked, categorized, severity-graded messages supporting read/dismissed tracking and optimized querying indexes. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.DateTime, call:sa.func.now, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:sa.text, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
||||||
- 2026_05_29_add_ssh_key_ids_to_tool_instances.py | Alembic database migration that adds a JSON column named ssh_key_ids to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.JSON, func:downgrade() → None, call:op.drop_column | dep: alembic, sqlalchemy
|
- 2026_05_29_add_ssh_key_ids_to_tool_instances.py | Alembic database migration that adds a JSON column named ssh_key_ids to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.JSON, func:downgrade() → None, call:op.drop_column | dep: alembic, sqlalchemy
|
||||||
@@ -44,20 +44,18 @@ Database schema evolution and versioning management for the API application usin
|
|||||||
- 2026_05_29_remove_lsio_command_override.py | Alembic database migration that removes broken command overrides containing --bind-addr or --host flags from LinuxServer.io code-server Docker Compose templates in both database tool_types records and on-disk instance compose files. | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text(""" SELECT id, compose_template FROM tool_types WHERE name = 'code-server' """) ).fetchall, call:sa.text, call:yaml.safe_load, call:data["services"].values, call:svc.get, call:yaml.dump, call:print, call:conn.execute( sa.text(""" SELECT column_name FROM information_schema.columns WHERE table_name = 'tool_instances' AND column_name = 'compose_path' """) ).fetchone, call:conn.execute( sa.text(""" SELECT id, compose_path FROM tool_instances WHERE compose_path IS NOT NULL """) ).fetchall, call:Path, call:path.exists, call:path.read_text, call:path.write_text, func:downgrade() → None | dep: collections.abc, alembic, yaml, pathlib, sqlalchemy, pathlib.Path, information_schema
|
- 2026_05_29_remove_lsio_command_override.py | Alembic database migration that removes broken command overrides containing --bind-addr or --host flags from LinuxServer.io code-server Docker Compose templates in both database tool_types records and on-disk instance compose files. | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text(""" SELECT id, compose_template FROM tool_types WHERE name = 'code-server' """) ).fetchall, call:sa.text, call:yaml.safe_load, call:data["services"].values, call:svc.get, call:yaml.dump, call:print, call:conn.execute( sa.text(""" SELECT column_name FROM information_schema.columns WHERE table_name = 'tool_instances' AND column_name = 'compose_path' """) ).fetchone, call:conn.execute( sa.text(""" SELECT id, compose_path FROM tool_instances WHERE compose_path IS NOT NULL """) ).fetchall, call:Path, call:path.exists, call:path.read_text, call:path.write_text, func:downgrade() → None | dep: collections.abc, alembic, yaml, pathlib, sqlalchemy, pathlib.Path, information_schema
|
||||||
- 2026_05_29_remove_ssh_keys_mount_from_manifest.py | Alembic database migration that removes (or restores) the ssh_keys mount from a JSON manifest stored in the tool_definition_manifests table for the pi-agent tool definition. | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:isinstance, call:json.loads, call:manifest.get, call:len, call:m.get, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:isinstance, call:json.loads, call:manifest.get, call:any, call:m.get, call:mounts.append, call:json.dumps | dep: json, typing, alembic, sqlalchemy
|
- 2026_05_29_remove_ssh_keys_mount_from_manifest.py | Alembic database migration that removes (or restores) the ssh_keys mount from a JSON manifest stored in the tool_definition_manifests table for the pi-agent tool definition. | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:isinstance, call:json.loads, call:manifest.get, call:len, call:m.get, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:isinstance, call:json.loads, call:manifest.get, call:any, call:m.get, call:mounts.append, call:json.dumps | dep: json, typing, alembic, sqlalchemy
|
||||||
- 2026_06_01_add_workspaces.py | Alembic database migration that creates a workspaces table with foreign keys to git_repositories and users, adds indexes, and adds a workspace_id column to tool_instances | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.ForeignKey, call:sa.DateTime, call:sa.text, call:sa.UniqueConstraint, call:op.create_index, call:op.add_column, func:downgrade() → None, call:op.drop_index, call:op.drop_column, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
- 2026_06_01_add_workspaces.py | Alembic database migration that creates a workspaces table with foreign keys to git_repositories and users, adds indexes, and adds a workspace_id column to tool_instances | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.ForeignKey, call:sa.DateTime, call:sa.text, call:sa.UniqueConstraint, call:op.create_index, call:op.add_column, func:downgrade() → None, call:op.drop_index, call:op.drop_column, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
||||||
- 2026_06_13_make_clone_mode_nullable.py | Alembic database migration that makes the `clone_mode` column in `tool_instances` table nullable to support workspace-first cleanup workflow. | exp: func:upgrade() → None, call:op.alter_column, call:sa.String, func:downgrade() → None, call:op.alter_column, call:sa.String | dep: alembic, sqlalchemy
|
- 2026_06_13_make_clone_mode_nullable.py | Alembic database migration that makes the `clone_mode` column in `tool_instances` table nullable to allow NULL values for new rows | exp: func:upgrade() → None, call:op.alter_column, call:sa.String, func:downgrade() → None, call:op.alter_column, call:sa.String | dep: alembic, sqlalchemy
|
||||||
- 2026_06_14_104415_add_tool_type_home_directory.py | Alembic database migration that adds a `home_directory` column to `tool_types` table and updates template strings to use a configurable workspace path instead of hardcoded `/workspace` | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:op.execute, call:sa.update(tool_types) .where(tool_types.c.compose_template.is_not(None)) .values, call:tool_types.c.compose_template.is_not, call:sa.func.replace, call:sa.update(tool_types) .where(tool_types.c.dockerfile_template.is_not(None)) .values, call:tool_types.c.dockerfile_template.is_not, func:downgrade() → None, call:op.execute, call:sa.update(tool_types) .where(tool_types.c.compose_template.is_not(None)) .values, call:tool_types.c.compose_template.is_not, call:sa.func.replace, call:sa.update(tool_types) .where(tool_types.c.dockerfile_template.is_not(None)) .values, call:tool_types.c.dockerfile_template.is_not, call:op.drop_column | dep: typing, alembic, sqlalchemy.sql, sqlalchemy
|
|
||||||
- 398082499c30_add_tool_config_fields.py | Alembic database migration that adds five new columns (port_override, start_command, working_directory, environment_variables, volumes) to the tool_configs table with a port range check constraint. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Integer, call:sa.Text, call:postgresql.JSONB, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:op.drop_constraint, call:op.drop_column | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
- 398082499c30_add_tool_config_fields.py | Alembic database migration that adds five new columns (port_override, start_command, working_directory, environment_variables, volumes) to the tool_configs table with a port range check constraint. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Integer, call:sa.Text, call:postgresql.JSONB, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:op.drop_constraint, call:op.drop_column | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||||
- 6fc7bfcf199f_merge_remove_is_builtin_and_add_config_.py | Alembic database migration that merges two parallel revision branches (removing is_builtin and adding config_profiles) into a single history line | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
- 6fc7bfcf199f_merge_remove_is_builtin_and_add_config_.py | Alembic database migration that merges two parallel revision branches (removing is_builtin and adding config_profiles) into a single history line | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
||||||
- 86cec91fdb00_merge_profile_resolver_and_workspaces_.py | Alembic database migration that merges two divergent migration branches (profile resolver and workspaces) into a single head | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
- 86cec91fdb00_merge_profile_resolver_and_workspaces_.py | Alembic database migration that merges two divergent migration branches (profile resolver and workspaces) into a single head | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
||||||
- 8c6d1dbd4798_remove_pi_config_and_state_mounts_from_.py | Alembic database migration that removes or restores pi_state and pi_config mounts from the pi-agent manifest stored in tool_definition_manifests table | exp: func:_load_manifest(manifest_json), call:isinstance, call:json.loads, func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:_load_manifest, call:manifest.get, call:len, call:m.get, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:_load_manifest, call:manifest.get, call:m.get, call:mounts.append, call:json.dumps | dep: json, alembic, sqlalchemy
|
- 8c6d1dbd4798_remove_pi_config_and_state_mounts_from_.py | Alembic database migration that removes pi_state and pi_config mounts from the pi-agent manifest in upgrade, and restores them in downgrade | exp: func:_load_manifest(manifest_json), call:isinstance, call:json.loads, func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:_load_manifest, call:manifest.get, call:len, call:m.get, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:_load_manifest, call:manifest.get, call:m.get, call:mounts.append, call:json.dumps | dep: json, alembic, sqlalchemy
|
||||||
- 8ed7dd80973d_create_config_folders_table.py | Alembic database migration that creates a config_folders table with user-owned configuration folders supporting JSONB file storage and project overrides | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.ForeignKey, call:sa.String, call:sa.Text, call:postgresql.JSONB, call:sa.Boolean, call:sa.DateTime, call:sa.UniqueConstraint, call:op.create_index, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
- 8ed7dd80973d_create_config_folders_table.py | Alembic database migration that creates a config_folders table with user-owned configuration folders supporting JSONB file storage and project overrides | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.ForeignKey, call:sa.String, call:sa.Text, call:postgresql.JSONB, call:sa.Boolean, call:sa.DateTime, call:sa.UniqueConstraint, call:op.create_index, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||||
- af8512103d67_add_tool_type_fields.py | Alembic database migration that adds new columns (definition_type, dockerfile_template, build_context, readiness_probe) to the tool_types table with a CHECK constraint on definition_type. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:sa.Text, call:postgresql.JSONB, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:op.drop_constraint, call:op.drop_column | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
- af8512103d67_add_tool_type_fields.py | Alembic database migration that adds new columns (definition_type, dockerfile_template, build_context, readiness_probe) to the tool_types table with a CHECK constraint on definition_type. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:sa.Text, call:postgresql.JSONB, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:op.drop_constraint, call:op.drop_column | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||||
- f3d2dc90ba3a_merge_single_interface_and_clone_mode.py | Alembic database migration that merges two prior revisions (single_interface and clone_mode) into a single migration path | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
|
- f3d2dc90ba3a_merge_single_interface_and_clone_mode.py | Alembic database migration that merges two prior revisions (single_interface and clone_mode) into a single migration path | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
|
||||||
- fc8f1a20cbf6_merge_home_directory_and_pi_agent_mount_.py | Alembic database migration that merges two revision branches by declaring them as down revisions without performing any schema changes. | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic, sqlalchemy
|
|
||||||
## arch
|
## arch
|
||||||
Sequential and branched migration pattern with merge resolution, using Alembic's revision-based approach with upgrade/downgrade functions, including data migrations, conditional schema changes, and cross-dialect support (PostgreSQL/SQLite).
|
Linear and branched migration pattern using Alembic's revision system with merge migrations to reconcile divergent branches; each migration is an imperative upgrade/downgrade script containing raw SQL/DDL operations, with some migrations including data seeding and dialect-specific logic (PostgreSQL/SQLite), but lacks consistent naming convention (mixed timestamp and numeric prefixes) indicating organic evolution rather than planned schema design.
|
||||||
## tags
|
## tags
|
||||||
column, table, call:op.drop, alembic, downgrade, upgrade, key, call:sa.text
|
column, table, call:op.drop, downgrade, alembic, upgrade, key, call:sa.text
|
||||||
## symbols
|
## symbols
|
||||||
- upgrade
|
- upgrade
|
||||||
- downgrade
|
- downgrade
|
||||||
|
|||||||
@@ -104,11 +104,11 @@ RUN git config --global init.defaultBranch main \\
|
|||||||
&& git config --global user.name "Developer"
|
&& git config --global user.name "Developer"
|
||||||
|
|
||||||
# Create default tmux config
|
# Create default tmux config
|
||||||
RUN printf '%s\\n' 'set -g mouse on' 'set -g default-terminal "screen-256color"' > /home/user/.tmux.conf
|
RUN echo 'set -g mouse on\\nset -g default-terminal "screen-256color"' > /home/user/.tmux.conf
|
||||||
|
|
||||||
# Create default ranger config
|
# Create default ranger config
|
||||||
RUN mkdir -p /home/user/.config/ranger \\
|
RUN mkdir -p /home/user/.config/ranger \\
|
||||||
&& printf '%s\\n' 'set preview_files true' 'set use_preview_script true' > /home/user/.config/ranger/rc.conf
|
&& echo 'set preview_files true\\nset use_preview_script true' > /home/user/.config/ranger/rc.conf
|
||||||
|
|
||||||
# Set up Pi config directory
|
# Set up Pi config directory
|
||||||
RUN mkdir -p /home/user/.pi/agent
|
RUN mkdir -p /home/user/.pi/agent
|
||||||
|
|||||||
@@ -308,10 +308,10 @@ RUN git config --global init.defaultBranch main \\
|
|||||||
&& git config --global user.email "dev@headquarter.local" \\
|
&& git config --global user.email "dev@headquarter.local" \\
|
||||||
&& git config --global user.name "Developer"
|
&& git config --global user.name "Developer"
|
||||||
|
|
||||||
RUN printf '%s\\n' 'set -g mouse on' 'set -g default-terminal "screen-256color"' > /home/user/.tmux.conf
|
RUN echo 'set -g mouse on\\nset -g default-terminal "screen-256color"' > /home/user/.tmux.conf
|
||||||
|
|
||||||
RUN mkdir -p /home/user/.config/ranger \\
|
RUN mkdir -p /home/user/.config/ranger \\
|
||||||
&& printf '%s\\n' 'set preview_files true' 'set use_preview_script true' > /home/user/.config/ranger/rc.conf
|
&& echo 'set preview_files true\\nset use_preview_script true' > /home/user/.config/ranger/rc.conf
|
||||||
|
|
||||||
RUN mkdir -p /home/user/.pi/agent
|
RUN mkdir -p /home/user/.pi/agent
|
||||||
|
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
"""add_tool_type_home_directory
|
|
||||||
|
|
||||||
Revision ID: 2026_06_14_104415
|
|
||||||
Revises: f3d2dc90ba3a
|
|
||||||
Create Date: 2026-06-14 10:44:15.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from alembic import op
|
|
||||||
from sqlalchemy.sql import column, table
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "2026_06_14_104415"
|
|
||||||
down_revision: Union[str, Sequence[str], None] = "f3d2dc90ba3a"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
tool_types = table(
|
|
||||||
"tool_types",
|
|
||||||
column("id", sa.UUID),
|
|
||||||
column("home_directory", sa.String),
|
|
||||||
column("compose_template", sa.Text),
|
|
||||||
column("dockerfile_template", sa.Text),
|
|
||||||
)
|
|
||||||
|
|
||||||
OLD_WORKSPACE = "/workspace"
|
|
||||||
NEW_WORKSPACE = "/home/user/{{WORKSPACE_NAME}}"
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
# Add the new column with a default that applies to existing rows.
|
|
||||||
op.add_column(
|
|
||||||
"tool_types",
|
|
||||||
sa.Column(
|
|
||||||
"home_directory",
|
|
||||||
sa.String(255),
|
|
||||||
nullable=False,
|
|
||||||
server_default="/home/user",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Rewrite legacy templates that mount the workspace at /workspace so they
|
|
||||||
# use the new configurable home directory and preserve the workspace name.
|
|
||||||
op.execute(
|
|
||||||
sa.update(tool_types)
|
|
||||||
.where(tool_types.c.compose_template.is_not(None))
|
|
||||||
.values(
|
|
||||||
compose_template=sa.func.replace(
|
|
||||||
tool_types.c.compose_template, OLD_WORKSPACE, NEW_WORKSPACE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
sa.update(tool_types)
|
|
||||||
.where(tool_types.c.dockerfile_template.is_not(None))
|
|
||||||
.values(
|
|
||||||
dockerfile_template=sa.func.replace(
|
|
||||||
tool_types.c.dockerfile_template, OLD_WORKSPACE, NEW_WORKSPACE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
# Restore the original /workspace strings before dropping the column.
|
|
||||||
op.execute(
|
|
||||||
sa.update(tool_types)
|
|
||||||
.where(tool_types.c.compose_template.is_not(None))
|
|
||||||
.values(
|
|
||||||
compose_template=sa.func.replace(
|
|
||||||
tool_types.c.compose_template, NEW_WORKSPACE, OLD_WORKSPACE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
sa.update(tool_types)
|
|
||||||
.where(tool_types.c.dockerfile_template.is_not(None))
|
|
||||||
.values(
|
|
||||||
dockerfile_template=sa.func.replace(
|
|
||||||
tool_types.c.dockerfile_template, NEW_WORKSPACE, OLD_WORKSPACE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
op.drop_column("tool_types", "home_directory")
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
"""merge home directory and pi agent mount cleanup heads
|
|
||||||
|
|
||||||
Revision ID: fc8f1a20cbf6
|
|
||||||
Revises: 2026_06_14_104415, 8c6d1dbd4798
|
|
||||||
Create Date: 2026-06-14 11:08:41.273502
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'fc8f1a20cbf6'
|
|
||||||
down_revision = ('2026_06_14_104415', '8c6d1dbd4798')
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
pass
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/src
|
dir: apps/api/src
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Core application bootstrap and infrastructure layer for the Headquarter API, providing configuration, database connectivity, logging, and FastAPI application initialization.
|
Core API application package that initializes and configures the Headquarter FastAPI backend with database, authentication, logging, and middleware infrastructure.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/.pi-map.index.md
|
index: apps/api/.pi-map.index.md
|
||||||
map: apps/api/.pi-map.md
|
map: apps/api/.pi-map.md
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ dir: apps/api/src
|
|||||||
index: apps/api/src/.pi-map.index.md
|
index: apps/api/src/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Core API application package for the Headquarter service, providing configuration, database connectivity, structured logging, and FastAPI application initialization.
|
Core API application package that initializes and configures the Headquarter FastAPI backend with database, authentication, logging, and middleware infrastructure.
|
||||||
## files
|
## files
|
||||||
- __init__.py | Marks the directory as a Python package for the Headquarter API.
|
- __init__.py | Marks the directory as a Python package for the Headquarter API.
|
||||||
- config.py | Defines application configuration settings with environment-based overrides using Pydantic, including database URLs, service domains, OAuth/Authentik integration, JWT/session settings, and computed properties for environment-specific behavior. | exp: class:Settings, func:build_database_url(user: str, password: str, host: str, port: int, database: str) → str | dep: pydantic, pydantic_settings
|
- config.py | Defines application configuration settings with environment-based overrides using Pydantic, including database URLs, service domains, OAuth/Authentik integration, JWT/session settings, and computed properties for environment-specific behavior. | exp: class:Settings, func:build_database_url(user: str, password: str, host: str, port: int, database: str) → str | dep: pydantic, pydantic_settings
|
||||||
@@ -12,7 +12,7 @@ Core API application package for the Headquarter service, providing configuratio
|
|||||||
- 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
|
- 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 | 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.*
|
- 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
|
## arch
|
||||||
Layered architecture with environment-based Pydantic configuration, async SQLAlchemy with Alembic migrations, structured JSON logging with correlation IDs, and FastAPI middleware/routing pattern with background services integration.
|
Layered architecture using Pydantic for environment-based configuration, async SQLAlchemy with Alembic migrations, structured JSON logging with correlation IDs, and FastAPI middleware/routing pattern for a service-oriented backend.
|
||||||
## tags
|
## tags
|
||||||
src, database, logging, call:logger.info, api, middleware, fastapi, filter
|
src, database, logging, call:logger.info, api, middleware, fastapi, filter
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/src/models
|
dir: apps/api/src/models
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides the SQLAlchemy ORM data models and database schema definitions for the API application.
|
Centralized database model definitions and shared infrastructure for the API's data layer.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/src/.pi-map.index.md
|
index: apps/api/src/.pi-map.index.md
|
||||||
map: apps/api/src/.pi-map.md
|
map: apps/api/src/.pi-map.md
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ dir: apps/api/src/models
|
|||||||
index: apps/api/src/models/.pi-map.index.md
|
index: apps/api/src/models/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides the SQLAlchemy ORM data models and database schema definitions for the API application.
|
Centralized database model definitions and shared infrastructure for the API's data layer.
|
||||||
## files
|
## files
|
||||||
- __init__.py | Re-exports model classes from submodules to provide a centralized public API for the src.models package | dep: src.models.base, src.models.config.config_profile, src.models.project.git_repository, src.models.project.project, src.models.project.workspace, src.models.system.health_check, src.models.system.instance_event, src.models.system.notification, src.models.system.terminal_session, src.models.tool.tool_definition_manifest, src.models.tool.tool_instance, src.models.tool.tool_type, src.models.user.ssh_key, src.models.user.user, src.models.user.user_config
|
- __init__.py | Re-exports model classes from submodules to provide a centralized public API for the src.models package | dep: src.models.base, src.models.config.config_profile, src.models.project.git_repository, src.models.project.project, src.models.project.workspace, src.models.system.health_check, src.models.system.instance_event, src.models.system.notification, src.models.system.terminal_session, src.models.tool.tool_definition_manifest, src.models.tool.tool_instance, src.models.tool.tool_type, src.models.user.ssh_key, src.models.user.user, src.models.user.user_config
|
||||||
- base.py | Defines SQLAlchemy base model and reusable mixins for UUID primary keys and automatic timestamp tracking in database models. | exp: class:Base, class:UUIDPrimaryKeyMixin, class:TimestampMixin | dep: uuid, datetime, sqlalchemy, sqlalchemy.orm
|
- base.py | Defines SQLAlchemy base model and reusable mixins for UUID primary keys and automatic timestamp tracking in database models. | exp: class:Base, class:UUIDPrimaryKeyMixin, class:TimestampMixin | dep: uuid, datetime, sqlalchemy, sqlalchemy.orm
|
||||||
## arch
|
## arch
|
||||||
Layered repository pattern with declarative SQLAlchemy base, UUID/timestamp mixins for reusable model traits, and package-level facade pattern via __init__.py re-exports to centralize model access.
|
SQLAlchemy ORM with declarative base, mixin-based composition for cross-cutting concerns (UUIDs, timestamps), and explicit package-level re-exports for clean public API surface.
|
||||||
## tags
|
## tags
|
||||||
models, src, base, project, system, user, mixin, tool
|
models, src, base, project, system, user, mixin, tool
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/src/models/tool
|
dir: apps/api/src/models/tool
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides SQLAlchemy ORM models for managing containerized tool definitions, types, and deployed instances in the API.
|
Database models for containerized tool lifecycle management, covering tool definitions, deployment instances, and categorization types.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/src/models/.pi-map.index.md
|
index: apps/api/src/models/.pi-map.index.md
|
||||||
map: apps/api/src/models/.pi-map.md
|
map: apps/api/src/models/.pi-map.md
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ dir: apps/api/src/models/tool
|
|||||||
index: apps/api/src/models/tool/.pi-map.index.md
|
index: apps/api/src/models/tool/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides SQLAlchemy ORM models for managing containerized tool definitions, types, and deployed instances in the API.
|
Database models for containerized tool lifecycle management, covering tool definitions, deployment instances, and categorization types.
|
||||||
## files
|
## files
|
||||||
- __init__.py | Exports the public API for the tool models module by re-exporting three key classes. | dep: src.models.tool.tool_definition_manifest, src.models.tool.tool_instance, src.models.tool.tool_type
|
- __init__.py | Exports the public API for the tool models module by re-exporting three key classes. | dep: src.models.tool.tool_definition_manifest, src.models.tool.tool_instance, src.models.tool.tool_type
|
||||||
- tool_definition_manifest.py | Defines a SQLAlchemy ORM model for storing tool definition manifests that compile to Dockerfiles and Compose files, supporting both base definitions and tool-specific definitions with inheritance. | exp: class:ToolDefinitionManifest | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.user
|
- tool_definition_manifest.py | Defines a SQLAlchemy ORM model for storing tool definition manifests that compile to Dockerfiles and Compose files, supporting both base definitions and tool-specific definitions with inheritance. | exp: class:ToolDefinitionManifest | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.user
|
||||||
- tool_instance.py | Defines a SQLAlchemy ORM model for tool instances that represent deployed tools with container metadata, status tracking, and relationships to users, projects, workspaces, and other entities. | exp: class:ToolInstance | dep: uuid, datetime, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models, src.models.project, src.models.user, src.models (ConfigProfile, GitRepository, Project, ToolType, User, Workspace)
|
- tool_instance.py | Defines a SQLAlchemy ORM model for tool instances that represent deployed tools with container metadata, status tracking, and relationships to users, projects, workspaces, and other entities. | exp: class:ToolInstance | dep: uuid, datetime, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models, src.models.project, src.models.user, src.models (ConfigProfile, GitRepository, Project, ToolType, User, Workspace)
|
||||||
- tool_type.py | Defines a SQLAlchemy ORM model for tool types that specify configuration templates and metadata for deployable tools in a containerized environment. | exp: class:ToolType | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.tool.tool_definition_manifest, src.models.user
|
- tool_type.py | Defines a SQLAlchemy ORM model for tool types that represent configurable categories of tools with deployment templates, manifest references, and metadata. | exp: class:ToolType | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.tool.tool_definition_manifest, src.models.user
|
||||||
## arch
|
## arch
|
||||||
Domain-driven data models using SQLAlchemy ORM with declarative base pattern, entity relationships, and inheritance support for tool manifest definitions.
|
SQLAlchemy ORM with declarative models using inheritance hierarchies, relationship mappings, and polymorphic manifest compilation for Docker/Compose deployment.
|
||||||
## tags
|
## tags
|
||||||
tool, models, src, sqlalchemy, orm, definition, base, manifest
|
tool, models, src, sqlalchemy, orm, definition, manifest, base
|
||||||
## symbols
|
## symbols
|
||||||
- ToolDefinitionManifest
|
- ToolDefinitionManifest
|
||||||
- ToolInstance
|
- ToolInstance
|
||||||
|
|||||||
@@ -39,9 +39,6 @@ class ToolType(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
|||||||
)
|
)
|
||||||
readiness_probe: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
readiness_probe: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||||
startup_command: Mapped[str | None] = mapped_column(Text, nullable=True)
|
startup_command: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
home_directory: Mapped[str] = mapped_column(
|
|
||||||
String(255), nullable=False, default="/home/user"
|
|
||||||
)
|
|
||||||
required_variables: Mapped[list[str]] = mapped_column(
|
required_variables: Mapped[list[str]] = mapped_column(
|
||||||
JSON, default=list, nullable=False
|
JSON, default=list, nullable=False
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/src/services
|
dir: apps/api/src/services
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Marks the services directory as a Python package for organizing business logic and service-layer abstractions.
|
Marks the services directory as a Python package for business logic layer components.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/src/.pi-map.index.md
|
index: apps/api/src/.pi-map.index.md
|
||||||
map: apps/api/src/.pi-map.md
|
map: apps/api/src/.pi-map.md
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ dir: apps/api/src/services
|
|||||||
index: apps/api/src/services/.pi-map.index.md
|
index: apps/api/src/services/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
This directory serves as a Python package namespace for organizing service-layer modules in the API application.
|
Provides a Python package namespace for organizing service-layer modules in the API application.
|
||||||
## files
|
## files
|
||||||
- __init__.py | Empty file with no functionality
|
- __init__.py | Empty file with no functionality
|
||||||
## arch
|
## arch
|
||||||
Standard Python package structure using __init__.py to define an importable module directory, following conventional layered architecture patterns.
|
Standard Python package structure using __init__.py for directory-based module organization, following conventional layered architecture patterns.
|
||||||
## tags
|
## tags
|
||||||
init, empty, functionality
|
init, empty, functionality
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from typing import Any
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from src.services.config.config_profile_resolver import expand_container_path
|
|
||||||
from src.services.docker import sort_volumes_by_specificity
|
from src.services.docker import sort_volumes_by_specificity
|
||||||
|
|
||||||
|
|
||||||
@@ -159,8 +158,6 @@ def compile_dockerfile(manifest: dict) -> str:
|
|||||||
|
|
||||||
# User creation
|
# User creation
|
||||||
user = manifest.get("user")
|
user = manifest.get("user")
|
||||||
home_dir = get_manifest_home_dir(manifest)
|
|
||||||
workspace_name = manifest.get("workspace_name", "{{WORKSPACE_NAME}}")
|
|
||||||
if user:
|
if user:
|
||||||
name = user["name"]
|
name = user["name"]
|
||||||
uid = user["uid"]
|
uid = user["uid"]
|
||||||
@@ -171,21 +168,22 @@ def compile_dockerfile(manifest: dict) -> str:
|
|||||||
lines.append(f" useradd -u {uid} -g {gid} {create_home}-s {shell} {name}")
|
lines.append(f" useradd -u {uid} -g {gid} {create_home}-s {shell} {name}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
# Set HOME and USER for runtime compatibility
|
# Set HOME and USER for runtime compatibility
|
||||||
lines.append(f"ENV HOME={home_dir}")
|
home = f"/home/{name}"
|
||||||
|
lines.append(f"ENV HOME={home}")
|
||||||
lines.append(f"ENV USER={name}")
|
lines.append(f"ENV USER={name}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
# Ensure home directory exists and is writable by the user.
|
# Ensure home directory exists and is writable by the user.
|
||||||
# Recursively chown so any files copied from /etc/skel by useradd -m
|
# Recursively chown so any files copied from /etc/skel by useradd -m
|
||||||
# (e.g. .bashrc, .config) are owned by the container user.
|
# (e.g. .bashrc, .config) are owned by the container user.
|
||||||
lines.append(
|
lines.append(
|
||||||
f"RUN mkdir -p {home_dir} && chown -R {name}:{name} {home_dir} && chmod 755 {home_dir}"
|
f"RUN mkdir -p {home} && chown -R {name}:{name} {home} && chmod 755 {home}"
|
||||||
)
|
)
|
||||||
# Pre-create common config directories so apps like ranger can write
|
# Pre-create common config directories so apps like ranger can write
|
||||||
# their configs on first run without permission errors.
|
# their configs on first run without permission errors.
|
||||||
common_dirs = [".config", ".local/share", ".cache"]
|
common_dirs = [".config", ".local/share", ".cache"]
|
||||||
for d in common_dirs:
|
for d in common_dirs:
|
||||||
lines.append(
|
lines.append(
|
||||||
f"RUN mkdir -p {home_dir}/{d} && chown -R {name}:{name} {home_dir}/{d}"
|
f"RUN mkdir -p {home}/{d} && chown -R {name}:{name} {home}/{d}"
|
||||||
)
|
)
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
@@ -210,12 +208,10 @@ def compile_dockerfile(manifest: dict) -> str:
|
|||||||
|
|
||||||
# After build scripts, ensure everything in home is owned by the user
|
# After build scripts, ensure everything in home is owned by the user
|
||||||
if user and build_scripts:
|
if user and build_scripts:
|
||||||
lines.append(f"RUN chown -R {name}:{name} {home_dir}")
|
lines.append(f"RUN chown -R {name}:{name} {home}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Create mount target directories and /workspace compatibility symlink.
|
# Create mount target directories
|
||||||
# The symlink target includes the workspace/repo name so legacy scripts
|
|
||||||
# that cd into /workspace still land on the right project.
|
|
||||||
mounts = manifest.get("mounts", [])
|
mounts = manifest.get("mounts", [])
|
||||||
if mounts:
|
if mounts:
|
||||||
dirs = [mount["target"] for mount in mounts]
|
dirs = [mount["target"] for mount in mounts]
|
||||||
@@ -225,16 +221,6 @@ def compile_dockerfile(manifest: dict) -> str:
|
|||||||
lines.append(f"RUN chown -R {user['name']}:{user['name']} {dir_str}")
|
lines.append(f"RUN chown -R {user['name']}:{user['name']} {dir_str}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
workspace_target = f"{home_dir}/{workspace_name}"
|
|
||||||
lines.append(f"RUN mkdir -p {workspace_target}")
|
|
||||||
if user:
|
|
||||||
lines.append(
|
|
||||||
f"RUN ln -sfn {workspace_target} /workspace && chown -R {user['name']}:{user['name']} {home_dir}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
lines.append(f"RUN ln -sfn {workspace_target} /workspace")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
# Entrypoint for startup scripts
|
# Entrypoint for startup scripts
|
||||||
startup_scripts = manifest.get("scripts", {}).get("startup", [])
|
startup_scripts = manifest.get("scripts", {}).get("startup", [])
|
||||||
if startup_scripts:
|
if startup_scripts:
|
||||||
@@ -247,18 +233,11 @@ def compile_dockerfile(manifest: dict) -> str:
|
|||||||
# Switch to runtime user
|
# Switch to runtime user
|
||||||
if user:
|
if user:
|
||||||
lines.append(f"USER {user['name']}")
|
lines.append(f"USER {user['name']}")
|
||||||
|
lines.append(f"WORKDIR /home/{user['name']}")
|
||||||
# Set WORKDIR to the configured home directory unless runtime.working_dir
|
lines.append("")
|
||||||
# explicitly overrides it.
|
|
||||||
runtime = manifest.get("runtime", {})
|
|
||||||
working_dir = runtime.get("working_dir")
|
|
||||||
if working_dir:
|
|
||||||
lines.append(f"WORKDIR {expand_container_path(working_dir, home_dir)}")
|
|
||||||
else:
|
|
||||||
lines.append(f"WORKDIR {home_dir}")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
# Entrypoint and CMD
|
# Entrypoint and CMD
|
||||||
|
runtime = manifest.get("runtime", {})
|
||||||
if startup_scripts:
|
if startup_scripts:
|
||||||
lines.append('ENTRYPOINT ["/usr/local/bin/headquarter-entrypoint"]')
|
lines.append('ENTRYPOINT ["/usr/local/bin/headquarter-entrypoint"]')
|
||||||
|
|
||||||
@@ -272,11 +251,6 @@ def compile_dockerfile(manifest: dict) -> str:
|
|||||||
def compile_entrypoint(manifest: dict) -> str:
|
def compile_entrypoint(manifest: dict) -> str:
|
||||||
"""Generate the startup entrypoint script from startup scripts.
|
"""Generate the startup entrypoint script from startup scripts.
|
||||||
|
|
||||||
Injects a permission-fixer preamble that runs as root (or via sudo) before
|
|
||||||
any user-defined startup script. It chowns the home directory and a safe
|
|
||||||
subset of mount parents to the container user, creates the /workspace
|
|
||||||
compatibility symlink, and avoids recursive chown of large repo subtrees.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
manifest: Fully resolved manifest JSON.
|
manifest: Fully resolved manifest JSON.
|
||||||
|
|
||||||
@@ -285,70 +259,6 @@ def compile_entrypoint(manifest: dict) -> str:
|
|||||||
"""
|
"""
|
||||||
lines = ["#!/bin/bash", "set -e", ""]
|
lines = ["#!/bin/bash", "set -e", ""]
|
||||||
|
|
||||||
user = manifest.get("user")
|
|
||||||
home_dir = get_manifest_home_dir(manifest)
|
|
||||||
workspace_name = manifest.get("workspace_name", "{{WORKSPACE_NAME}}")
|
|
||||||
workspace_target = f"{home_dir}/{workspace_name}"
|
|
||||||
|
|
||||||
# Permission fixer preamble: run as root when possible, else fall back to
|
|
||||||
# passwordless sudo configured in the Dockerfile.
|
|
||||||
lines.append("# Permission fixer preamble")
|
|
||||||
lines.append("CONTAINER_USER=''")
|
|
||||||
lines.append('if [ "$(id -u)" = '"'"'0'"'"' ]; then')
|
|
||||||
if user:
|
|
||||||
lines.append(f" CONTAINER_USER='{user['name']}'")
|
|
||||||
lines.append("else")
|
|
||||||
lines.append(" # Try passwordless sudo; ignore failure so the container still starts")
|
|
||||||
lines.append(" if sudo -n true 2>/dev/null; then")
|
|
||||||
lines.append(" SUDO='sudo'")
|
|
||||||
lines.append(" else")
|
|
||||||
lines.append(" SUDO=''")
|
|
||||||
lines.append(" fi")
|
|
||||||
lines.append("fi")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
if user:
|
|
||||||
name = user["name"]
|
|
||||||
uid = user["uid"]
|
|
||||||
gid = user["gid"]
|
|
||||||
lines.append(f"USER_NAME='{name}'")
|
|
||||||
lines.append(f"USER_UID='{uid}'")
|
|
||||||
lines.append(f"USER_GID='{gid}'")
|
|
||||||
lines.append(f"HOME_DIR='{home_dir}'")
|
|
||||||
lines.append(f"WORKSPACE_TARGET='{workspace_target}'")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("fix_owner() {")
|
|
||||||
lines.append(" local path=\"$1\"")
|
|
||||||
lines.append(' [ -e "$path" ] || return 0')
|
|
||||||
lines.append(' if [ -n "$SUDO" ]; then')
|
|
||||||
lines.append(' sudo chown "$USER_UID:$USER_GID" "$path" 2>/dev/null || true')
|
|
||||||
lines.append(' elif [ "$(id -u)" = "0" ]; then')
|
|
||||||
lines.append(' chown "$USER_UID:$USER_GID" "$path" 2>/dev/null || true')
|
|
||||||
lines.append(' fi')
|
|
||||||
lines.append("}")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("# Ensure home directory exists and is owned by the container user")
|
|
||||||
lines.append('mkdir -p "$HOME_DIR"')
|
|
||||||
lines.append('fix_owner "$HOME_DIR"')
|
|
||||||
lines.append("")
|
|
||||||
lines.append("# Ensure workspace target exists and is owned by the container user")
|
|
||||||
lines.append('mkdir -p "$WORKSPACE_TARGET"')
|
|
||||||
lines.append('fix_owner "$WORKSPACE_TARGET"')
|
|
||||||
lines.append("")
|
|
||||||
lines.append("# Create /workspace compatibility symlink")
|
|
||||||
lines.append('ln -sfn "$WORKSPACE_TARGET" /workspace')
|
|
||||||
lines.append("")
|
|
||||||
lines.append("# Fix ownership of declared mount targets (top-level only)")
|
|
||||||
for mount in manifest.get("mounts", []):
|
|
||||||
target = mount.get("target")
|
|
||||||
if not target:
|
|
||||||
continue
|
|
||||||
# Expand any ~/$HOME placeholders in the mount target.
|
|
||||||
expanded = target.replace("~", home_dir).replace("$HOME", home_dir)
|
|
||||||
if expanded.startswith(home_dir) and not mount.get("readonly", False):
|
|
||||||
lines.append(f'fix_owner "{expanded}"')
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
startup_scripts = manifest.get("scripts", {}).get("startup", [])
|
startup_scripts = manifest.get("scripts", {}).get("startup", [])
|
||||||
for script in startup_scripts:
|
for script in startup_scripts:
|
||||||
lines.append(script)
|
lines.append(script)
|
||||||
@@ -371,13 +281,6 @@ def compile_compose(manifest: dict, variables: dict[str, Any]) -> str:
|
|||||||
runtime = manifest.get("runtime", {})
|
runtime = manifest.get("runtime", {})
|
||||||
user = manifest.get("user")
|
user = manifest.get("user")
|
||||||
interface_type = manifest["interface_type"]
|
interface_type = manifest["interface_type"]
|
||||||
home_dir = get_manifest_home_dir(manifest)
|
|
||||||
|
|
||||||
# Determine the workspace/repo name from variables when available.
|
|
||||||
workspace_name = variables.get(
|
|
||||||
"WORKSPACE_NAME",
|
|
||||||
variables.get("REPO_NAME", "workspace"),
|
|
||||||
)
|
|
||||||
|
|
||||||
service: dict[str, Any] = {
|
service: dict[str, Any] = {
|
||||||
"image": variables["IMAGE_TAG"],
|
"image": variables["IMAGE_TAG"],
|
||||||
@@ -391,9 +294,7 @@ def compile_compose(manifest: dict, variables: dict[str, Any]) -> str:
|
|||||||
if runtime.get("tty", False):
|
if runtime.get("tty", False):
|
||||||
service["tty"] = True
|
service["tty"] = True
|
||||||
if runtime.get("working_dir"):
|
if runtime.get("working_dir"):
|
||||||
service["working_dir"] = expand_container_path(
|
service["working_dir"] = runtime["working_dir"]
|
||||||
runtime["working_dir"], home_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
# User override
|
# User override
|
||||||
if user:
|
if user:
|
||||||
@@ -418,24 +319,14 @@ def compile_compose(manifest: dict, variables: dict[str, Any]) -> str:
|
|||||||
|
|
||||||
# Volumes from mount schema
|
# Volumes from mount schema
|
||||||
volumes = []
|
volumes = []
|
||||||
has_explicit_repo_mount = False
|
|
||||||
for mount in manifest.get("mounts", []):
|
for mount in manifest.get("mounts", []):
|
||||||
source = resolve_mount_source(mount, variables)
|
source = resolve_mount_source(mount, variables)
|
||||||
if not source:
|
if not source:
|
||||||
continue
|
continue
|
||||||
if mount.get("source_type") == "repo":
|
target = mount["target"]
|
||||||
has_explicit_repo_mount = True
|
|
||||||
target = expand_container_path(mount["target"], home_dir)
|
|
||||||
readonly = ":ro" if mount.get("readonly", False) else ""
|
readonly = ":ro" if mount.get("readonly", False) else ""
|
||||||
volumes.append(f"{source}:{target}{readonly}")
|
volumes.append(f"{source}:{target}{readonly}")
|
||||||
|
|
||||||
# Synthesize a default repo/workspace mount when the manifest does not
|
|
||||||
# declare an explicit repo mount. This preserves the repo root directory
|
|
||||||
# name under the configured home directory.
|
|
||||||
if not has_explicit_repo_mount and variables.get("REPO_PATH"):
|
|
||||||
target = f"{home_dir}/{workspace_name}"
|
|
||||||
volumes.append(f"{variables['REPO_PATH']}:{target}")
|
|
||||||
|
|
||||||
# Append extra volumes from tool config / config profile
|
# Append extra volumes from tool config / config profile
|
||||||
for vol in variables.get("EXTRA_VOLUMES", []):
|
for vol in variables.get("EXTRA_VOLUMES", []):
|
||||||
vol_str = f"{vol['source']}:{vol['target']}"
|
vol_str = f"{vol['source']}:{vol['target']}"
|
||||||
@@ -494,12 +385,7 @@ def resolve_mount_source(mount: dict, variables: dict[str, Any]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def get_manifest_home_dir(manifest: dict) -> str:
|
def get_manifest_home_dir(manifest: dict) -> str:
|
||||||
"""Get the home directory for a container based on manifest config.
|
"""Get the home directory for a container based on manifest user config.
|
||||||
|
|
||||||
Precedence:
|
|
||||||
1. manifest["home_directory"] if present and non-empty.
|
|
||||||
2. /home/{user.name} if manifest.user.name is present.
|
|
||||||
3. /root otherwise.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
manifest: Fully resolved manifest JSON.
|
manifest: Fully resolved manifest JSON.
|
||||||
@@ -507,10 +393,6 @@ def get_manifest_home_dir(manifest: dict) -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
Home directory path (e.g., /home/user or /root).
|
Home directory path (e.g., /home/user or /root).
|
||||||
"""
|
"""
|
||||||
home_directory = manifest.get("home_directory")
|
|
||||||
if home_directory and isinstance(home_directory, str) and home_directory.strip():
|
|
||||||
return home_directory.strip()
|
|
||||||
|
|
||||||
user = manifest.get("user")
|
user = manifest.get("user")
|
||||||
if user and user.get("name"):
|
if user and user.get("name"):
|
||||||
return f"/home/{user['name']}"
|
return f"/home/{user['name']}"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/src/services/docker
|
dir: apps/api/src/services/docker
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides Docker infrastructure services for container lifecycle management, compose orchestration, secure configuration deployment, and external tunnel exposure.
|
Provides Docker infrastructure services for container lifecycle management, compose orchestration, configuration deployment, and secure tunneling to expose internal services.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/src/services/.pi-map.index.md
|
index: apps/api/src/services/.pi-map.index.md
|
||||||
map: apps/api/src/services/.pi-map.md
|
map: apps/api/src/services/.pi-map.md
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ dir: apps/api/src/services/docker
|
|||||||
index: apps/api/src/services/docker/.pi-map.index.md
|
index: apps/api/src/services/docker/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides Docker infrastructure services for container lifecycle management, compose orchestration, secure configuration deployment, and external tunnel exposure.
|
Provides Docker infrastructure services for container lifecycle management, compose orchestration, configuration deployment, and secure tunneling to expose internal services.
|
||||||
## files
|
## files
|
||||||
- __init__.py | Package initialization file that exposes Docker-related service functions for container operations, compose management, configuration staging, and tunnel management. | dep: src.services.docker.compose, src.services.docker.config_staging, src.services.docker.container, src.services.docker.tunnel
|
- __init__.py | Package initialization file that exposes Docker-related service functions for container operations, compose management, configuration staging, and tunnel management. | dep: src.services.docker.compose, src.services.docker.config_staging, src.services.docker.container, src.services.docker.tunnel
|
||||||
- compose.py | Generates, renders, and executes Docker Compose files with volume sorting and template variable substitution. | exp: func:sort_volumes_by_specificity(volumes: list[str]) → list[str], call:vol.split, call:len, call:parts[1].rstrip, call:target.count, call:targets.append, call:Counter(targets).items, call:logger.warning, call:sorted, func:_target_depth(vol: str) → int, call:vol.split, call:len, call:parts[1].rstrip, call:target.count, func:render_compose_template(template: str, variables: dict[str, Any]) → str, call:variables.items, call:result.replace, call:str, call:aliases.items, call:variables.get, func:write_compose_file(instance_dir: str, content: str) → str, call:Path, call:compose_path.write_text, call:str, func:execute_compose_command(compose_path: str, action: str, timeout, env_file) → tuple[int, str, str], call:Path, call:cmd.extend, call:cmd.append, call:subprocess.run, call:str, raise:ValueError | dep: logging, subprocess, collections, pathlib, typing, collections.Counter, pathlib.Path, typing.Any
|
- compose.py | Generates, renders, and executes Docker Compose commands for container orchestration with volume sorting and template substitution. | exp: func:sort_volumes_by_specificity(volumes: list[str]) → list[str], call:vol.split, call:len, call:parts[1].rstrip, call:target.count, call:targets.append, call:Counter(targets).items, call:logger.warning, call:sorted, func:_target_depth(vol: str) → int, call:vol.split, call:len, call:parts[1].rstrip, call:target.count, func:render_compose_template(template: str, variables: dict[str, Any]) → str, call:variables.items, call:result.replace, call:str, func:write_compose_file(instance_dir: str, content: str) → str, call:Path, call:compose_path.write_text, call:str, func:execute_compose_command(compose_path: str, action: str, timeout, env_file) → tuple[int, str, str], call:Path, call:cmd.extend, call:cmd.append, call:subprocess.run, call:str, raise:ValueError | dep: logging, subprocess, collections, pathlib, typing, collections.Counter, pathlib.Path, typing.Any
|
||||||
- config_staging.py | Stages configuration files into instance directories with security checks for path traversal. | exp: func:ensure_instance_directory(instance_id: str, base_path) → str, call:Settings, call:Path, call:instance_dir.mkdir, call:str, call:instance_dir.absolute, func:write_env_file(instance_dir: str, env_vars: dict[str, str]) → str, call:Path, call:env_vars.items, call:env_path.write_text, call:"\n".join, call:str, func:write_config_files(instance_dir: str, files: dict[str, str]) → None, call:Path, call:files.items, call:full_path.resolve().relative_to, call:instance_path.resolve, call:full_path.parent.mkdir, call:full_path.write_text, raise:ValueError | dep: logging, pathlib, src.config, src.config.Settings
|
- config_staging.py | Stages configuration files into instance directories with security checks for path traversal. | exp: func:ensure_instance_directory(instance_id: str, base_path) → str, call:Settings, call:Path, call:instance_dir.mkdir, call:str, call:instance_dir.absolute, func:write_env_file(instance_dir: str, env_vars: dict[str, str]) → str, call:Path, call:env_vars.items, call:env_path.write_text, call:"\n".join, call:str, func:write_config_files(instance_dir: str, files: dict[str, str]) → None, call:Path, call:files.items, call:full_path.resolve().relative_to, call:instance_path.resolve, call:full_path.parent.mkdir, call:full_path.write_text, raise:ValueError | dep: logging, pathlib, src.config, src.config.Settings
|
||||||
- container.py | Provides Docker container runtime queries and network management utilities via subprocess calls to the Docker CLI. | exp: func:get_container_id(instance_name: str) → str | None, call:instance_name.lower, call:subprocess.run, call:result.stdout.strip, call:ps_result.stdout.strip().splitlines, call:line.split, call:len, call:name.lower, func:get_container_name(instance_name: str) → str | None, call:subprocess.run, call:instance_name.lower, call:result.stdout.strip().lstrip, func:get_backend_network_name() → str, call:subprocess.run, call:result.stdout.strip().split, call:net.lower, func:connect_container_to_network(container_name: str, network_name) → bool, call:get_backend_network_name, call:subprocess.run, func:get_container_ip_on_network(container_id: str, network_name) → str | None, call:get_backend_network_name, call:subprocess.run, call:result.stdout.strip, func:is_container_on_network(container_id: str, network_name) → bool, call:get_backend_network_name, call:subprocess.run, func:get_container_status(container_id: str) → dict[str, Any], call:subprocess.run, call:result.stdout.strip().split, call:int, call:len, call:parts[1].isdigit, func:wait_for_container_running(container_id: str, timeout, interval) → dict[str, Any], call:time.time, call:get_container_status, call:time.sleep, func:get_container_logs(container_id: str, tail) → str, call:subprocess.run, call:str, func:find_free_port(start, end) → int, call:range, call:socket.socket, call:s.connect_ex, raise:RuntimeError | dep: logging, subprocess, time, typing, socket
|
- container.py | Provides Docker container runtime queries and network management utilities via subprocess calls to the Docker CLI. | exp: func:get_container_id(instance_name: str) → str | None, call:instance_name.lower, call:subprocess.run, call:result.stdout.strip, call:ps_result.stdout.strip().splitlines, call:line.split, call:len, call:name.lower, func:get_container_name(instance_name: str) → str | None, call:subprocess.run, call:instance_name.lower, call:result.stdout.strip().lstrip, func:get_backend_network_name() → str, call:subprocess.run, call:result.stdout.strip().split, call:net.lower, func:connect_container_to_network(container_name: str, network_name) → bool, call:get_backend_network_name, call:subprocess.run, func:get_container_ip_on_network(container_id: str, network_name) → str | None, call:get_backend_network_name, call:subprocess.run, call:result.stdout.strip, func:is_container_on_network(container_id: str, network_name) → bool, call:get_backend_network_name, call:subprocess.run, func:get_container_status(container_id: str) → dict[str, Any], call:subprocess.run, call:result.stdout.strip().split, call:int, call:len, call:parts[1].isdigit, func:wait_for_container_running(container_id: str, timeout, interval) → dict[str, Any], call:time.time, call:get_container_status, call:time.sleep, func:get_container_logs(container_id: str, tail) → str, call:subprocess.run, call:str, func:find_free_port(start, end) → int, call:range, call:socket.socket, call:s.connect_ex, raise:RuntimeError | dep: logging, subprocess, time, typing, socket
|
||||||
- tunnel.py | Manages Cloudflare tunnels by orchestrating cloudflared Docker containers to expose internal services via temporary public URLs. | exp: func:_tunnel_container_name(instance_name: str) → str, call:instance_name.lower, func:_ensure_image() → None, call:subprocess.run, call:result.stdout.strip, call:logger.info, call:logger.warning, func:_cleanup_stale_tunnel(tunnel_name: str) → None, call:subprocess.run, func:_get_tunnel_logs(tunnel_name: str) → tuple[str, str], call:subprocess.run, func:_get_tunnel_exit_code(tunnel_name: str) → int | None, call:subprocess.run, call:int, call:result.stdout.strip, func:start_tunnel(instance_name: str, container_port: int, timeout, target_url) → dict[str, str], call:_ensure_image, call:_tunnel_container_name, call:_cleanup_stale_tunnel, call:instance_name.lower, call:get_backend_network_name, call:logger.debug, call:" ".join, call:subprocess.run, call:proc.stdout.strip, call:re.compile, call:__import__("time").time, call:_get_tunnel_logs, call:url_pattern.search, call:match.group, call:_get_tunnel_exit_code, call:__import__("time").sleep, call:logger.info, raise:RuntimeError, func:stop_tunnel(instance_name: str) → None, call:_tunnel_container_name, call:_cleanup_stale_tunnel, call:logger.debug, func:recreate_tunnel(instance_name: str, container_port: int, target_url) → dict[str, str], call:stop_tunnel, call:start_tunnel, func:check_tunnel_health(url: str, timeout) → dict[str, Any], call:subprocess.run, call:int, call:result.stdout.strip, call:str(exc).lower, call:any | dep: logging, re, subprocess, typing, src.services.docker.container
|
- tunnel.py | Manages Cloudflare tunnels by orchestrating cloudflared Docker containers to expose internal services via temporary public URLs. | exp: func:_tunnel_container_name(instance_name: str) → str, call:instance_name.lower, func:_ensure_image() → None, call:subprocess.run, call:result.stdout.strip, call:logger.info, call:logger.warning, func:_cleanup_stale_tunnel(tunnel_name: str) → None, call:subprocess.run, func:_get_tunnel_logs(tunnel_name: str) → tuple[str, str], call:subprocess.run, func:_get_tunnel_exit_code(tunnel_name: str) → int | None, call:subprocess.run, call:int, call:result.stdout.strip, func:start_tunnel(instance_name: str, container_port: int, timeout, target_url) → dict[str, str], call:_ensure_image, call:_tunnel_container_name, call:_cleanup_stale_tunnel, call:instance_name.lower, call:get_backend_network_name, call:logger.debug, call:" ".join, call:subprocess.run, call:proc.stdout.strip, call:re.compile, call:__import__("time").time, call:_get_tunnel_logs, call:url_pattern.search, call:match.group, call:_get_tunnel_exit_code, call:__import__("time").sleep, call:logger.info, raise:RuntimeError, func:stop_tunnel(instance_name: str) → None, call:_tunnel_container_name, call:_cleanup_stale_tunnel, call:logger.debug, func:recreate_tunnel(instance_name: str, container_port: int, target_url) → dict[str, str], call:stop_tunnel, call:start_tunnel, func:check_tunnel_health(url: str, timeout) → dict[str, Any], call:subprocess.run, call:int, call:result.stdout.strip, call:str(exc).lower, call:any | dep: logging, re, subprocess, typing, src.services.docker.container
|
||||||
## arch
|
## arch
|
||||||
Service-oriented utility modules with subprocess-based Docker CLI integration, Jinja2 templating for compose generation, and security-hardened file operations with path traversal validation.
|
Subprocess-based CLI wrapper architecture around Docker/cloudflared tools with template rendering, path-traversal-safe file staging, and functional decomposition into single-responsibility modules.
|
||||||
## tags
|
## tags
|
||||||
tunnel, container, call:subprocess.run, get, name, call:, network, call:result.stdout.strip
|
tunnel, container, call:subprocess.run, get, name, call:, network, call:result.stdout.strip
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -61,19 +61,6 @@ def render_compose_template(template: str, variables: dict[str, Any]) -> str:
|
|||||||
for key, value in variables.items():
|
for key, value in variables.items():
|
||||||
placeholder = f"{{{{{key}}}}}"
|
placeholder = f"{{{{{key}}}}}"
|
||||||
result = result.replace(placeholder, str(value))
|
result = result.replace(placeholder, str(value))
|
||||||
|
|
||||||
# Convenience aliases so legacy and migrated templates can use lowercase
|
|
||||||
# placeholders without changing every stored template.
|
|
||||||
aliases = {
|
|
||||||
"{{workspace_name}}": "WORKSPACE_NAME",
|
|
||||||
"{{home_directory}}": "HOME_DIRECTORY",
|
|
||||||
}
|
|
||||||
for alias_placeholder, key in aliases.items():
|
|
||||||
if alias_placeholder in result:
|
|
||||||
result = result.replace(
|
|
||||||
alias_placeholder, str(variables.get(key, "workspace"))
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/src/services/tool
|
dir: apps/api/src/services/tool
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Orchestrates end-to-end deployment and runtime management of development tool instances via containerized environments with remote access capabilities.
|
Provides backend infrastructure for provisioning and managing isolated development tool instances with their dependencies and network access.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/src/services/.pi-map.index.md
|
index: apps/api/src/services/.pi-map.index.md
|
||||||
map: apps/api/src/services/.pi-map.md
|
map: apps/api/src/services/.pi-map.md
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -9,6 +9,7 @@ import subprocess
|
|||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
import httpx
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@@ -32,6 +33,7 @@ from src.services.docker import (
|
|||||||
get_container_id,
|
get_container_id,
|
||||||
get_container_ip_on_network,
|
get_container_ip_on_network,
|
||||||
get_container_logs,
|
get_container_logs,
|
||||||
|
get_container_status,
|
||||||
is_container_on_network,
|
is_container_on_network,
|
||||||
render_compose_template,
|
render_compose_template,
|
||||||
sort_volumes_by_specificity,
|
sort_volumes_by_specificity,
|
||||||
@@ -62,9 +64,10 @@ from src.services.shared.permission_fixer import (
|
|||||||
apply_ssh_permissions,
|
apply_ssh_permissions,
|
||||||
)
|
)
|
||||||
from src.services.shared.readiness_probe import execute_probe
|
from src.services.shared.readiness_probe import execute_probe
|
||||||
from src.services.shared.ssh_keys import prepare_ssh_key_files
|
from src.services.shared.ssh_keys import cleanup_ssh_key_files, prepare_ssh_key_files
|
||||||
from src.services.instance.event_bus import InstanceEventBus
|
from src.services.instance.event_bus import InstanceEventBus
|
||||||
from src.services.instance.lifecycle_hooks import publish_lifecycle_event
|
from src.services.instance.lifecycle_hooks import publish_lifecycle_event
|
||||||
|
from src.auth.dependencies import _get_owned_project, _get_user
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
_event_bus = InstanceEventBus()
|
_event_bus = InstanceEventBus()
|
||||||
@@ -136,7 +139,7 @@ async def resolve_git_mounts(
|
|||||||
if isinstance(result, Exception):
|
if isinstance(result, Exception):
|
||||||
logger.warning("Git mount failed: %s", result)
|
logger.warning("Git mount failed: %s", result)
|
||||||
continue
|
continue
|
||||||
if isinstance(result, list):
|
if result:
|
||||||
volume_mounts.extend(result)
|
volume_mounts.extend(result)
|
||||||
|
|
||||||
return volume_mounts
|
return volume_mounts
|
||||||
@@ -1040,11 +1043,7 @@ async def create_tool_instance(
|
|||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
|
|
||||||
home_dir = tool_type.home_directory or "/home/user"
|
compose_content = f"""version: "3.8"\nservices:\n app:\n image: {image_tag}\n container_name: {instance_name.lower()}\n stdin_open: true\n tty: true\n{ports_section} volumes:\n - {repo_path}:/workspace\n restart: unless-stopped\n"""
|
||||||
repo_name = os.path.basename(os.path.normpath(repo_path))
|
|
||||||
workspace_target = f"{home_dir}/{repo_name}"
|
|
||||||
|
|
||||||
compose_content = f"""version: "3.8"\nservices:\n app:\n image: {image_tag}\n container_name: {instance_name.lower()}\n stdin_open: true\n tty: true\n{ports_section} environment:\n - HOME={home_dir}\n volumes:\n - {repo_path}:{workspace_target}\n working_dir: {workspace_target}\n restart: unless-stopped\n"""
|
|
||||||
write_compose_file(instance_dir, compose_content)
|
write_compose_file(instance_dir, compose_content)
|
||||||
|
|
||||||
elif tool_type.definition_type == "manifest":
|
elif tool_type.definition_type == "manifest":
|
||||||
@@ -1096,8 +1095,6 @@ async def create_tool_instance(
|
|||||||
"TOOL_PORT": tool_port,
|
"TOOL_PORT": tool_port,
|
||||||
"USER_ID": str(user_id),
|
"USER_ID": str(user_id),
|
||||||
"PROJECT_ID": str(project_id),
|
"PROJECT_ID": str(project_id),
|
||||||
"WORKSPACE_NAME": os.path.basename(os.path.normpath(repo_path)),
|
|
||||||
"HOME_DIRECTORY": tool_type.home_directory or "/home/user",
|
|
||||||
}
|
}
|
||||||
compose_content = render_compose_template(
|
compose_content = render_compose_template(
|
||||||
tool_type.compose_template, variables
|
tool_type.compose_template, variables
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/tests
|
dir: apps/api/tests
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides shared test infrastructure and fixtures for the API application's test suite.
|
Provides shared test infrastructure and fixtures for the FastAPI API application.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/.pi-map.index.md
|
index: apps/api/.pi-map.index.md
|
||||||
map: apps/api/.pi-map.md
|
map: apps/api/.pi-map.md
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ dir: apps/api/tests
|
|||||||
index: apps/api/tests/.pi-map.index.md
|
index: apps/api/tests/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides shared test infrastructure and fixtures for the API application's test suite.
|
Provides shared test infrastructure and fixtures for the FastAPI API application.
|
||||||
## files
|
## files
|
||||||
- conftest.py | Provides shared pytest fixtures for testing a FastAPI application with async SQLite database, authenticated clients, and test data setup. | exp: func:test_client() → Generator[TestClient, None, None], call:create_async_engine, call:engine.begin, call:conn.run_sync, call:asyncio.run, call:init_db, call:async_sessionmaker, call:patch, call:TestClient, call:app.dependency_overrides.pop, call:engine.dispose, func:init_db(), call:engine.begin, call:conn.run_sync, func:override_get_db_session() → AsyncGenerator[AsyncSession, None], call:async_sessionmaker, func:db_session(test_client) → AsyncGenerator[AsyncSession, None], call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:gen.aclose, call:create_async_engine, call:engine.begin, call:conn.run_sync, call:async_sessionmaker, call:engine.dispose, func:authenticated_client(test_client) → Generator[TestClient, None, None], call:str, call:uuid.uuid4, call:Settings, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, call:asyncio.run, call:create_test_user, call:create_session_cookie, call:test_client.cookies.set, func:create_test_user(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, func:test_project_and_repo(authenticated_client) → tuple[str, str], call:uuid.uuid4, call:Settings, call:authenticated_client.cookies.get, call:decode_session_cookie, call:uuid.UUID, call:asyncio.run, call:get_user_id, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:Project, call:session.add, call:GitRepository, call:session.commit, call:gen.aclose, call:create_project_and_repo, call:str, raise:RuntimeError, func:get_user_id(), call:Settings, call:authenticated_client.cookies.get, call:decode_session_cookie, call:uuid.UUID, func:create_project_and_repo(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:Project, call:session.add, call:GitRepository, call:session.commit, call:gen.aclose, func:admin_client(test_client) → Generator[TestClient, None, None], call:str, call:uuid.uuid4, call:Settings, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, call:asyncio.run, call:create_admin_user, call:create_session_cookie, call:test_client.cookies.set, func:create_admin_user(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose | dep: asyncio, os, typing, unittest.mock, pytest, pytest_asyncio, fastapi.testclient, sqlalchemy.ext.asyncio, src.config, src.models.base, src.main, src.auth.dependencies, uuid, src.auth.session, src.models.user.user, src.models.project.project, src.models.project.git_repository, fastapi, sqlalchemy, aiosqlite, src.models, src.auth
|
- conftest.py | Provides shared pytest fixtures for testing a FastAPI application with async SQLite database, authenticated clients, and test data setup. | exp: func:test_client() → Generator[TestClient, None, None], call:create_async_engine, call:engine.begin, call:conn.run_sync, call:asyncio.run, call:init_db, call:async_sessionmaker, call:patch, call:TestClient, call:app.dependency_overrides.pop, call:engine.dispose, func:init_db(), call:engine.begin, call:conn.run_sync, func:override_get_db_session() → AsyncGenerator[AsyncSession, None], call:async_sessionmaker, func:db_session(test_client) → AsyncGenerator[AsyncSession, None], call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:gen.aclose, call:create_async_engine, call:engine.begin, call:conn.run_sync, call:async_sessionmaker, call:engine.dispose, func:authenticated_client(test_client) → Generator[TestClient, None, None], call:str, call:uuid.uuid4, call:Settings, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, call:asyncio.run, call:create_test_user, call:create_session_cookie, call:test_client.cookies.set, func:create_test_user(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, func:test_project_and_repo(authenticated_client) → tuple[str, str], call:uuid.uuid4, call:Settings, call:authenticated_client.cookies.get, call:decode_session_cookie, call:uuid.UUID, call:asyncio.run, call:get_user_id, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:Project, call:session.add, call:GitRepository, call:session.commit, call:gen.aclose, call:create_project_and_repo, call:str, raise:RuntimeError, func:get_user_id(), call:Settings, call:authenticated_client.cookies.get, call:decode_session_cookie, call:uuid.UUID, func:create_project_and_repo(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:Project, call:session.add, call:GitRepository, call:session.commit, call:gen.aclose, func:admin_client(test_client) → Generator[TestClient, None, None], call:str, call:uuid.uuid4, call:Settings, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, call:asyncio.run, call:create_admin_user, call:create_session_cookie, call:test_client.cookies.set, func:create_admin_user(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose | dep: asyncio, os, typing, unittest.mock, pytest, pytest_asyncio, fastapi.testclient, sqlalchemy.ext.asyncio, src.config, src.models.base, src.main, src.auth.dependencies, uuid, src.auth.session, src.models.user.user, src.models.project.project, src.models.project.git_repository, fastapi, sqlalchemy, aiosqlite, src.models, src.auth
|
||||||
## arch
|
## arch
|
||||||
Pytest plugin architecture with dependency-injected async fixtures for database, HTTP client, and authentication state management.
|
Pytest fixture-based testing architecture using async SQLite in-memory database, dependency injection overrides, and async HTTP client setup for isolated integration tests.
|
||||||
## tags
|
## tags
|
||||||
call:app.dependency, call:create, overrides.get, call:override, fn, call:gen.asend, call:gen.aclose, user
|
call:app.dependency, call:create, overrides.get, call:override, fn, call:gen.asend, call:gen.aclose, user
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/tests/integration
|
dir: apps/api/tests/integration
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Integration and unit test suite for the API backend, covering authentication, CRUD APIs, workspace operations, git integration, notifications, and database model validation against real PostgreSQL and mocked dependencies.
|
Integration test suite for the API backend, covering authentication, workspaces, projects, notifications, configuration, and database models with real PostgreSQL and git repositories.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/tests/.pi-map.index.md
|
index: apps/api/tests/.pi-map.index.md
|
||||||
map: apps/api/tests/.pi-map.md
|
map: apps/api/tests/.pi-map.md
|
||||||
@@ -21,7 +21,6 @@ map: apps/api/tests/.pi-map.md
|
|||||||
- test_projects_api.py
|
- test_projects_api.py
|
||||||
- test_seed.py
|
- test_seed.py
|
||||||
- test_ssh_keys_api.py
|
- test_ssh_keys_api.py
|
||||||
- test_tool_instance_lifecycle.py
|
|
||||||
- test_tool_types_api.py
|
- test_tool_types_api.py
|
||||||
- test_tool_types_api_extended.py
|
- test_tool_types_api_extended.py
|
||||||
- test_users_api.py
|
- test_users_api.py
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ dir: apps/api/tests/integration
|
|||||||
index: apps/api/tests/integration/.pi-map.index.md
|
index: apps/api/tests/integration/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Integration and unit test suite for the API backend, covering authentication, CRUD APIs, workspace operations, git integration, notifications, and database model validation against real PostgreSQL and mocked dependencies.
|
Integration test suite for the API backend, covering authentication, workspaces, projects, notifications, configuration, and database models with real PostgreSQL and git repositories.
|
||||||
## files
|
## files
|
||||||
- __init__.py | Empty file with no functionality
|
- __init__.py | Empty file with no functionality
|
||||||
- test_auth_api.py | Integration tests for authentication API endpoints using a real PostgreSQL database | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, func:_prepare_auth_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:importlib.reload, func:_insert_test_user(user_id: str) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:test_login_redirects_to_authentik_authorize_endpoint() → None, call:_prepare_auth_test_db, call:_load_app, call:TestClient, call:client.get, func:test_me_returns_401_without_session_cookie() → None, call:_prepare_auth_test_db, call:_load_app, call:TestClient, call:client.get, func:test_me_returns_user_with_valid_session() → None, call:_prepare_auth_test_db, call:_insert_test_user, call:_load_app, call:Settings, call:create_session_cookie, call:TestClient, call:client.get, call:response.json, func:test_logout_clears_session_cookie() → None, call:_prepare_auth_test_db, call:_load_app, call:TestClient, call:client.post, call:response.headers.get | dep: uuid, asyncio, importlib, fastapi.testclient, pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.user.user, asyncpg, fastapi
|
- test_auth_api.py | Integration tests for authentication API endpoints using a real PostgreSQL database | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, func:_prepare_auth_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:importlib.reload, func:_insert_test_user(user_id: str) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:test_login_redirects_to_authentik_authorize_endpoint() → None, call:_prepare_auth_test_db, call:_load_app, call:TestClient, call:client.get, func:test_me_returns_401_without_session_cookie() → None, call:_prepare_auth_test_db, call:_load_app, call:TestClient, call:client.get, func:test_me_returns_user_with_valid_session() → None, call:_prepare_auth_test_db, call:_insert_test_user, call:_load_app, call:Settings, call:create_session_cookie, call:TestClient, call:client.get, call:response.json, func:test_logout_clears_session_cookie() → None, call:_prepare_auth_test_db, call:_load_app, call:TestClient, call:client.post, call:response.headers.get | dep: uuid, asyncio, importlib, fastapi.testclient, pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.user.user, asyncpg, fastapi
|
||||||
@@ -18,7 +18,6 @@ Integration and unit test suite for the API backend, covering authentication, CR
|
|||||||
- test_projects_api.py | Integration tests for a FastAPI projects API endpoint, verifying authentication, CRUD operations, and ownership-based authorization against a real PostgreSQL database. | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, func:_prepare_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:hasattr, call:asyncio.run, call:database_module.engine.dispose, call:importlib.reload, func:_mint_token(user_id: str) → str, call:Settings, call:create_session_cookie, func:_insert_user(user_id: str, email) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:_insert_project(project_id: str, owner_id: str, name) → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:Project, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:Project, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:test_create_project_requires_authentication() → None, call:_prepare_test_db, call:_load_app, call:TestClient, call:client.post, func:test_create_project_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, call:response.json, func:test_list_projects_returns_only_owned_projects() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, call:response.json, call:len, func:test_update_project_requires_ownership() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.patch, func:test_update_project_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.patch, call:response.json, func:test_delete_project_requires_ownership() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete, func:test_delete_project_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete, func:test_set_default_ssh_key_requires_ownership() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.patch | dep: uuid, datetime, asyncio, pytest, fastapi.testclient, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.project.project, src.models.user.user, asyncpg, importlib, fastapi, src.database, src.api.user.auth, src.api.project.projects, src.main
|
- test_projects_api.py | Integration tests for a FastAPI projects API endpoint, verifying authentication, CRUD operations, and ownership-based authorization against a real PostgreSQL database. | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, func:_prepare_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:hasattr, call:asyncio.run, call:database_module.engine.dispose, call:importlib.reload, func:_mint_token(user_id: str) → str, call:Settings, call:create_session_cookie, func:_insert_user(user_id: str, email) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:_insert_project(project_id: str, owner_id: str, name) → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:Project, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:Project, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:test_create_project_requires_authentication() → None, call:_prepare_test_db, call:_load_app, call:TestClient, call:client.post, func:test_create_project_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, call:response.json, func:test_list_projects_returns_only_owned_projects() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, call:response.json, call:len, func:test_update_project_requires_ownership() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.patch, func:test_update_project_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.patch, call:response.json, func:test_delete_project_requires_ownership() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete, func:test_delete_project_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete, func:test_set_default_ssh_key_requires_ownership() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.patch | dep: uuid, datetime, asyncio, pytest, fastapi.testclient, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.project.project, src.models.user.user, asyncpg, importlib, fastapi, src.database, src.api.user.auth, src.api.project.projects, src.main
|
||||||
- test_seed.py | Tests deterministic seed user generation and database seeding functionality for development environments. | exp: func:test_build_seed_user_returns_deterministic_payload() → None, call:build_seed_user, func:test_seed_database_creates_development_user(db_session: AsyncSession) → None, call:seed_database, call:db_session.scalar, call:select(User).where | dep: pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.models.user, src.scripts.seed
|
- test_seed.py | Tests deterministic seed user generation and database seeding functionality for development environments. | exp: func:test_build_seed_user_returns_deterministic_payload() → None, call:build_seed_user, func:test_seed_database_creates_development_user(db_session: AsyncSession) → None, call:seed_database, call:db_session.scalar, call:select(User).where | dep: pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.models.user, src.scripts.seed
|
||||||
- test_ssh_keys_api.py | Integration tests verifying SSH key API endpoints require authentication | exp: func:test_create_ssh_key_requires_authentication(test_client: TestClient) → None, call:test_client.post, func:test_list_ssh_keys_requires_authentication(test_client: TestClient) → None, call:test_client.get | dep: pytest, fastapi.testclient
|
- test_ssh_keys_api.py | Integration tests verifying SSH key API endpoints require authentication | exp: func:test_create_ssh_key_requires_authentication(test_client: TestClient) → None, call:test_client.post, func:test_list_ssh_keys_requires_authentication(test_client: TestClient) → None, call:test_client.get | dep: pytest, fastapi.testclient
|
||||||
- test_tool_instance_lifecycle.py | Integration test placeholder for verifying Docker availability and container home directory lifecycle behavior | exp: func:_docker_available() → bool, call:subprocess.run, func:test_docker_available_placeholder() → None, call:_docker_available | dep: subprocess, pytest
|
|
||||||
- test_tool_types_api.py | Integration tests for a FastAPI tool types REST API endpoint using a real PostgreSQL database. | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, func:_prepare_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:hasattr, call:asyncio.run, call:database_module.engine.dispose, call:importlib.reload, func:_mint_token(user_id: str) → str, call:Settings, call:create_session_cookie, call:datetime.now, call:timedelta, func:_insert_user(user_id: str, email) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:_insert_tool_type(tool_type_id: str, name: str, display_name: str, compose_template: str, created_by_id) → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:ToolType, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:ToolType, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:test_list_tool_types_requires_authentication() → None, call:_prepare_test_db, call:_load_app, call:TestClient, call:client.get, func:test_list_tool_types_returns_all_types() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, call:response.json, call:len, call:next, func:test_get_tool_type_by_id() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, call:response.json, func:test_get_tool_type_not_found() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, func:test_create_tool_type_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, call:response.json, func:test_create_tool_type_duplicate_name() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_create_tool_type_invalid_yaml() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_create_tool_type_missing_services() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_create_tool_type_missing_required_variable() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_update_tool_type_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.put, call:response.json, func:test_update_tool_type_not_found() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.put, func:test_delete_tool_type_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete, call:client.get, func:test_delete_tool_type_not_found() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete | dep: uuid, datetime, asyncio, pytest, fastapi.testclient, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.tool.tool_type, src.models.user.user, asyncpg, importlib, fastapi
|
- test_tool_types_api.py | Integration tests for a FastAPI tool types REST API endpoint using a real PostgreSQL database. | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, func:_prepare_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:hasattr, call:asyncio.run, call:database_module.engine.dispose, call:importlib.reload, func:_mint_token(user_id: str) → str, call:Settings, call:create_session_cookie, call:datetime.now, call:timedelta, func:_insert_user(user_id: str, email) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:_insert_tool_type(tool_type_id: str, name: str, display_name: str, compose_template: str, created_by_id) → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:ToolType, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:ToolType, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:test_list_tool_types_requires_authentication() → None, call:_prepare_test_db, call:_load_app, call:TestClient, call:client.get, func:test_list_tool_types_returns_all_types() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, call:response.json, call:len, call:next, func:test_get_tool_type_by_id() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, call:response.json, func:test_get_tool_type_not_found() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, func:test_create_tool_type_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, call:response.json, func:test_create_tool_type_duplicate_name() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_create_tool_type_invalid_yaml() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_create_tool_type_missing_services() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_create_tool_type_missing_required_variable() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_update_tool_type_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.put, call:response.json, func:test_update_tool_type_not_found() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.put, func:test_delete_tool_type_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete, call:client.get, func:test_delete_tool_type_not_found() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete | dep: uuid, datetime, asyncio, pytest, fastapi.testclient, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.tool.tool_type, src.models.user.user, asyncpg, importlib, fastapi
|
||||||
- test_tool_types_api_extended.py | Integration tests for a FastAPI tool types API endpoint covering CRUD operations with extended fields like dockerfile templates, readiness probes, startup commands, and validation. | exp: class:TestToolTypesAPIExtended, method:test_create_tool_type_with_dockerfile(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_create_tool_type_with_readiness_probe(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_create_tool_type_invalid_definition_type(self, authenticated_client: TestClient) → None, call:authenticated_client.post, method:test_create_tool_type_dockerfile_without_template(self, authenticated_client: TestClient) → None, call:authenticated_client.post, method:test_update_tool_type_with_new_fields(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.put, call:response.json, method:test_validate_tool_type_compose(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_validate_tool_type_invalid_compose(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_validate_tool_type_dockerfile(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_get_tool_type_returns_new_fields(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.get, call:response.json, method:test_create_tool_type_without_port_fails(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, call:str, method:test_create_tool_type_with_port_mismatch_fails(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_create_tool_type_with_startup_command(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_update_tool_type_startup_command(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.put, call:response.json, method:test_get_tool_type_returns_startup_command(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.get, call:response.json | dep: pytest, fastapi.testclient, fastapi.testclient.TestClient
|
- test_tool_types_api_extended.py | Integration tests for a FastAPI tool types API endpoint covering CRUD operations with extended fields like dockerfile templates, readiness probes, startup commands, and validation. | exp: class:TestToolTypesAPIExtended, method:test_create_tool_type_with_dockerfile(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_create_tool_type_with_readiness_probe(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_create_tool_type_invalid_definition_type(self, authenticated_client: TestClient) → None, call:authenticated_client.post, method:test_create_tool_type_dockerfile_without_template(self, authenticated_client: TestClient) → None, call:authenticated_client.post, method:test_update_tool_type_with_new_fields(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.put, call:response.json, method:test_validate_tool_type_compose(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_validate_tool_type_invalid_compose(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_validate_tool_type_dockerfile(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_get_tool_type_returns_new_fields(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.get, call:response.json, method:test_create_tool_type_without_port_fails(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, call:str, method:test_create_tool_type_with_port_mismatch_fails(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_create_tool_type_with_startup_command(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_update_tool_type_startup_command(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.put, call:response.json, method:test_get_tool_type_returns_startup_command(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.get, call:response.json | dep: pytest, fastapi.testclient, fastapi.testclient.TestClient
|
||||||
- test_users_api.py | Integration tests for user profile API endpoints using a real PostgreSQL database | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, func:_prepare_users_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:importlib.reload, func:_insert_test_user(user_id: str) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:_create_auth_cookie(user_id: str) → str, call:Settings, call:create_session_cookie, func:test_get_profile_returns_401_without_cookie() → None, call:_prepare_users_test_db, call:_load_app, call:TestClient, call:client.get, func:test_get_profile_returns_user_data() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.get, call:response.json, func:test_update_profile_changes_name_and_email() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.put, call:response.json, func:test_update_profile_rejects_empty_name() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.put, func:test_update_profile_rejects_invalid_email() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.put, func:test_upload_avatar_updates_avatar_url() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.post, call:io.BytesIO, call:response.json, call:data["avatar_url"].startswith, func:test_upload_avatar_rejects_invalid_file_type() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.post, call:io.BytesIO, func:test_upload_avatar_rejects_oversized_file() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.post, call:io.BytesIO | dep: uuid, datetime, asyncio, io, fastapi.testclient, pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.user.user, asyncpg, importlib, fastapi, src.database, src.api.user.users, src.main
|
- test_users_api.py | Integration tests for user profile API endpoints using a real PostgreSQL database | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, func:_prepare_users_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:importlib.reload, func:_insert_test_user(user_id: str) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:_create_auth_cookie(user_id: str) → str, call:Settings, call:create_session_cookie, func:test_get_profile_returns_401_without_cookie() → None, call:_prepare_users_test_db, call:_load_app, call:TestClient, call:client.get, func:test_get_profile_returns_user_data() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.get, call:response.json, func:test_update_profile_changes_name_and_email() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.put, call:response.json, func:test_update_profile_rejects_empty_name() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.put, func:test_update_profile_rejects_invalid_email() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.put, func:test_upload_avatar_updates_avatar_url() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.post, call:io.BytesIO, call:response.json, call:data["avatar_url"].startswith, func:test_upload_avatar_rejects_invalid_file_type() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.post, call:io.BytesIO, func:test_upload_avatar_rejects_oversized_file() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.post, call:io.BytesIO | dep: uuid, datetime, asyncio, io, fastapi.testclient, pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.user.user, asyncpg, importlib, fastapi, src.database, src.api.user.users, src.main
|
||||||
@@ -27,7 +26,7 @@ Integration and unit test suite for the API backend, covering authentication, CR
|
|||||||
- test_workspace_instances.py | Integration tests for FastAPI workspace instance endpoints (list and create) using an authenticated test client and mocked database fixtures. | exp: class:TestListWorkspaceInstances, method:test_list_empty(self, authenticated_client: TestClient, test_workspace_with_tool_type), call:authenticated_client.get, call:response.json, method:test_list_instances(self, authenticated_client: TestClient, db_session: AsyncSession, test_workspace_with_tool_type), call:ToolInstance, call:db_session.add, call:db_session.commit, call:asyncio.run, call:_create_instance, call:authenticated_client.get, call:response.json, call:len, class:TestCreateWorkspaceInstance, method:test_create_instance(self, authenticated_client: TestClient, test_workspace_with_tool_type), call:ToolInstance, call:uuid.uuid4, call:datetime.now, call:patch, call:authenticated_client.post, call:str, call:response.json, func:_get_user_id(client: TestClient) → uuid.UUID, call:Settings, call:client.cookies.get, call:decode_session_cookie, call:uuid.UUID, raise:RuntimeError, func:test_workspace_with_tool_type(db_session: AsyncSession, authenticated_client: TestClient), call:_get_user_id, call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:tempfile.mkdtemp, call:Workspace, call:ToolType, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_create, func:_create(), call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:tempfile.mkdtemp, call:Workspace, call:ToolType, call:db_session.commit, call:db_session.refresh | dep: asyncio, tempfile, uuid, datetime, unittest.mock, pytest, fastapi.testclient, sqlalchemy.ext.asyncio, src.models, src.auth.session, src.config, src.api.workspace.workspace_instances
|
- test_workspace_instances.py | Integration tests for FastAPI workspace instance endpoints (list and create) using an authenticated test client and mocked database fixtures. | exp: class:TestListWorkspaceInstances, method:test_list_empty(self, authenticated_client: TestClient, test_workspace_with_tool_type), call:authenticated_client.get, call:response.json, method:test_list_instances(self, authenticated_client: TestClient, db_session: AsyncSession, test_workspace_with_tool_type), call:ToolInstance, call:db_session.add, call:db_session.commit, call:asyncio.run, call:_create_instance, call:authenticated_client.get, call:response.json, call:len, class:TestCreateWorkspaceInstance, method:test_create_instance(self, authenticated_client: TestClient, test_workspace_with_tool_type), call:ToolInstance, call:uuid.uuid4, call:datetime.now, call:patch, call:authenticated_client.post, call:str, call:response.json, func:_get_user_id(client: TestClient) → uuid.UUID, call:Settings, call:client.cookies.get, call:decode_session_cookie, call:uuid.UUID, raise:RuntimeError, func:test_workspace_with_tool_type(db_session: AsyncSession, authenticated_client: TestClient), call:_get_user_id, call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:tempfile.mkdtemp, call:Workspace, call:ToolType, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_create, func:_create(), call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:tempfile.mkdtemp, call:Workspace, call:ToolType, call:db_session.commit, call:db_session.refresh | dep: asyncio, tempfile, uuid, datetime, unittest.mock, pytest, fastapi.testclient, sqlalchemy.ext.asyncio, src.models, src.auth.session, src.config, src.api.workspace.workspace_instances
|
||||||
- test_workspaces_api.py | Integration tests for workspace API endpoints including list, create, delete, and sync operations with repository/project scoping | exp: class:TestListWorkspaces, method:test_list_empty(self, authenticated_client: TestClient, test_repo: GitRepository), call:authenticated_client.get, call:response.json, method:test_list_with_workspaces(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:asyncio.run, call:_commit, call:authenticated_client.get, call:response.json, call:len, class:TestCreateWorkspace, method:test_create_success(self, authenticated_client: TestClient, test_repo: GitRepository), call:Workspace, call:uuid.uuid4, call:patch.object, call:authenticated_client.post, call:response.json, call:mock_create.assert_called_once, method:test_create_missing_name(self, authenticated_client: TestClient, test_repo: GitRepository), call:authenticated_client.post, call:response.json, method:test_create_duplicate_name(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:asyncio.run, call:_commit, call:patch.object, call:Exception, call:authenticated_client.post, class:TestDeleteWorkspace, method:test_delete_without_instances(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:authenticated_client.delete, call:response.json, method:test_delete_with_instances_force(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:authenticated_client.delete, class:TestSyncWorkspace, method:test_sync_success(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:MagicMock, call:authenticated_client.post, call:response.json, method:test_sync_branch_deleted(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:MagicMock, call:authenticated_client.post, call:response.json, func:_get_user_id_from_client(client: TestClient) → uuid.UUID, call:Settings, call:client.cookies.get, call:decode_session_cookie, call:uuid.UUID, raise:RuntimeError, func:test_repo(db_session: AsyncSession, authenticated_client: TestClient), call:_get_user_id_from_client, call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_create, func:_create(), call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:db_session.commit, call:db_session.refresh | dep: asyncio, uuid, unittest.mock, pytest, fastapi.testclient, sqlalchemy.ext.asyncio, src.models, src.services.shared.workspace_manager, src.auth.session, src.config, fastapi, sqlalchemy
|
- test_workspaces_api.py | Integration tests for workspace API endpoints including list, create, delete, and sync operations with repository/project scoping | exp: class:TestListWorkspaces, method:test_list_empty(self, authenticated_client: TestClient, test_repo: GitRepository), call:authenticated_client.get, call:response.json, method:test_list_with_workspaces(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:asyncio.run, call:_commit, call:authenticated_client.get, call:response.json, call:len, class:TestCreateWorkspace, method:test_create_success(self, authenticated_client: TestClient, test_repo: GitRepository), call:Workspace, call:uuid.uuid4, call:patch.object, call:authenticated_client.post, call:response.json, call:mock_create.assert_called_once, method:test_create_missing_name(self, authenticated_client: TestClient, test_repo: GitRepository), call:authenticated_client.post, call:response.json, method:test_create_duplicate_name(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:asyncio.run, call:_commit, call:patch.object, call:Exception, call:authenticated_client.post, class:TestDeleteWorkspace, method:test_delete_without_instances(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:authenticated_client.delete, call:response.json, method:test_delete_with_instances_force(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:authenticated_client.delete, class:TestSyncWorkspace, method:test_sync_success(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:MagicMock, call:authenticated_client.post, call:response.json, method:test_sync_branch_deleted(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:MagicMock, call:authenticated_client.post, call:response.json, func:_get_user_id_from_client(client: TestClient) → uuid.UUID, call:Settings, call:client.cookies.get, call:decode_session_cookie, call:uuid.UUID, raise:RuntimeError, func:test_repo(db_session: AsyncSession, authenticated_client: TestClient), call:_get_user_id_from_client, call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_create, func:_create(), call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:db_session.commit, call:db_session.refresh | dep: asyncio, uuid, unittest.mock, pytest, fastapi.testclient, sqlalchemy.ext.asyncio, src.models, src.services.shared.workspace_manager, src.auth.session, src.config, fastapi, sqlalchemy
|
||||||
## arch
|
## arch
|
||||||
Pytest-based integration testing with FastAPI TestClient, SQLAlchemy async sessions, real PostgreSQL database fixtures, and selective mocking; follows pattern of endpoint-per-test-file with authenticated client fixtures and seeded database state.
|
Pytest-based integration testing using real external dependencies (PostgreSQL, git repos), FastAPI TestClient with authenticated fixtures, mocked database fixtures for isolation, and security validation patterns (cookie/ OIDC/ SSE auth).
|
||||||
## tags
|
## tags
|
||||||
test, call:, call:db, call:authenticated, user, create, call:response.json, call:create
|
test, call:, call:db, call:authenticated, user, create, call:response.json, call:create
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
"""Integration tests for tool container home directory behavior.
|
|
||||||
|
|
||||||
These tests exercise container lifecycle behavior and require Docker and a
|
|
||||||
running PostgreSQL database. They are skipped when Docker is unavailable.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
def _docker_available() -> bool:
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["docker", "info"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
return result.returncode == 0
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
|
||||||
@pytest.mark.skipif(not _docker_available(), reason="Docker not available")
|
|
||||||
def test_docker_available_placeholder() -> None:
|
|
||||||
"""Placeholder to keep the test file valid when Docker is present.
|
|
||||||
|
|
||||||
Real lifecycle tests (container starts with HOME=/home/user, /workspace
|
|
||||||
symlink works, git mounts are writable) should be added here once the
|
|
||||||
test harness can start the API and Docker services.
|
|
||||||
"""
|
|
||||||
assert _docker_available()
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/tests/unit
|
dir: apps/api/tests/unit
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Unit test suite for the API application covering core services, utilities, and infrastructure components.
|
Contains comprehensive unit tests for the API backend services covering configuration, Docker operations, Git integration, file management, health monitoring, notifications, and SSH key handling.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/tests/.pi-map.index.md
|
index: apps/api/tests/.pi-map.index.md
|
||||||
map: apps/api/tests/.pi-map.md
|
map: apps/api/tests/.pi-map.md
|
||||||
@@ -10,7 +10,6 @@ map: apps/api/tests/.pi-map.md
|
|||||||
-
|
-
|
||||||
## files
|
## files
|
||||||
- __init__.py
|
- __init__.py
|
||||||
- test_alembic_migrations.py
|
|
||||||
- test_config.py
|
- test_config.py
|
||||||
- test_config_profile_resolver.py
|
- test_config_profile_resolver.py
|
||||||
- test_docker_build.py
|
- test_docker_build.py
|
||||||
@@ -22,7 +21,6 @@ map: apps/api/tests/.pi-map.md
|
|||||||
- test_git_url_parser.py
|
- test_git_url_parser.py
|
||||||
- test_health_monitor.py
|
- test_health_monitor.py
|
||||||
- test_home_path_expansion.py
|
- test_home_path_expansion.py
|
||||||
- test_instance_service.py
|
|
||||||
- test_lifecycle_hooks.py
|
- test_lifecycle_hooks.py
|
||||||
- test_manifest_compiler.py
|
- test_manifest_compiler.py
|
||||||
- test_migration_metadata.py
|
- test_migration_metadata.py
|
||||||
@@ -37,7 +35,7 @@ index: apps/api/tests/unit/.pi-map.index.md
|
|||||||
map: apps/api/tests/unit/.pi-map.md
|
map: apps/api/tests/unit/.pi-map.md
|
||||||
## workflows
|
## workflows
|
||||||
- change unit behavior
|
- change unit behavior
|
||||||
read: __init__.py, test_alembic_migrations.py, test_config.py
|
read: __init__.py, test_config.py, test_config_profile_resolver.py
|
||||||
- change unit config
|
- change unit config
|
||||||
read: test_config.py, test_config_profile_resolver.py
|
read: test_config.py, test_config_profile_resolver.py
|
||||||
## dirty
|
## dirty
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ dir: apps/api/tests/unit
|
|||||||
index: apps/api/tests/unit/.pi-map.index.md
|
index: apps/api/tests/unit/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Unit test suite for the API application covering core services, utilities, and infrastructure components.
|
Contains comprehensive unit tests for the API backend services covering configuration, Docker operations, Git integration, file management, health monitoring, notifications, and SSH key handling.
|
||||||
## files
|
## files
|
||||||
- __init__.py | Empty file with no functionality
|
- __init__.py | Empty file with no functionality
|
||||||
- test_alembic_migrations.py | Unit tests that verify Alembic database migrations are importable, have correct identifiers, and declare expected dependencies without requiring a live database. | exp: func:test_home_directory_migration_imports_and_rewrites() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable, func:test_merge_migration_resolves_heads() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable | dep: importlib.util, pathlib, pytest, importlib
|
|
||||||
- test_config.py | Tests configuration settings and database URL building for an application, verifying defaults, environment variable overrides, and environment-specific behavior. | exp: func:test_settings_default_database_url_uses_asyncpg(monkeypatch) → None, call:monkeypatch.delenv, call:Settings, func:test_build_database_url_uses_explicit_values() → None, call:build_database_url, func:test_settings_prefers_explicit_database_url_env(monkeypatch) → None, call:monkeypatch.setenv, call:Settings, func:test_auth_settings_have_secure_defaults() → None, call:Settings, call:settings.resolved_authentik_authorize_url.endswith, call:settings.resolved_authentik_token_url.endswith, call:settings.resolved_authentik_jwks_url.endswith, func:test_cookie_policy_is_strict_in_production(monkeypatch) → None, call:monkeypatch.setenv, call:Settings, func:test_cookie_policy_is_relaxed_for_local_dev(monkeypatch) → None, call:monkeypatch.setenv, call:Settings | dep: pytest, src.config, src.database
|
- test_config.py | Tests configuration settings and database URL building for an application, verifying defaults, environment variable overrides, and environment-specific behavior. | exp: func:test_settings_default_database_url_uses_asyncpg(monkeypatch) → None, call:monkeypatch.delenv, call:Settings, func:test_build_database_url_uses_explicit_values() → None, call:build_database_url, func:test_settings_prefers_explicit_database_url_env(monkeypatch) → None, call:monkeypatch.setenv, call:Settings, func:test_auth_settings_have_secure_defaults() → None, call:Settings, call:settings.resolved_authentik_authorize_url.endswith, call:settings.resolved_authentik_token_url.endswith, call:settings.resolved_authentik_jwks_url.endswith, func:test_cookie_policy_is_strict_in_production(monkeypatch) → None, call:monkeypatch.setenv, call:Settings, func:test_cookie_policy_is_relaxed_for_local_dev(monkeypatch) → None, call:monkeypatch.setenv, call:Settings | dep: pytest, src.config, src.database
|
||||||
- test_config_profile_resolver.py | Tests the config profile resolution system including merge helpers, profile inheritance with cycle detection, and git mount normalization | exp: class:TestMergeFunctions, method:test_merge_env_vars_basic(self) → None, call:_merge_env_vars, method:test_merge_env_vars_tracks_overrides(self) → None, call:_merge_env_vars, method:test_merge_runtime_hints_basic(self) → None, call:_merge_runtime_hints, method:test_merge_files_basic(self) → None, call:_merge_files, method:test_merge_mounts_basic(self) → None, call:_merge_mounts, method:test_merge_mounts_file_override(self) → None, call:_merge_mounts, call:ResolvedMount, method:test_merge_mounts_mode_conflict(self) → None, call:_merge_mounts, call:ResolvedMount, method:test_merge_git_mounts_basic(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_concatenate_same_repo_branch(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_dedup_same_mapping(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_different_repos(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_different_branches(self) → None, call:_merge_git_mounts, call:len, call:m.get, class:TestResolveProfile, class:TestApplyResolvedProfile, method:test_mounts_directory_not_individual_files(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:Path(volumes[0]["source"]).is_dir, call:(Path(volumes[0]["source"]) / "config.json").exists, call:(Path(volumes[0]["source"]) / "nested" / "file.txt").exists, method:test_directory_mount_target(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:Path, call:(Path(volumes[0]["source"]) / "z.json").exists, method:test_empty_mount_produces_no_volumes(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, method:test_home_expansion_in_directory_mount_target(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:(Path(volumes[0]["source"]) / "app.toml").exists, call:Path, method:test_readonly_mount_sets_readonly_flag(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:volumes[0].get, method:test_writable_mount_does_not_set_readonly_flag(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:volumes[0].get, class:TestCheckIncludeCycle | dep: uuid, pathlib, pytest, sqlalchemy.ext.asyncio, src.models.config.config_profile, src.services.config.config_profile_resolver
|
- test_config_profile_resolver.py | Tests the config profile resolution system including merge helpers, profile inheritance with cycle detection, and git mount normalization | exp: class:TestMergeFunctions, method:test_merge_env_vars_basic(self) → None, call:_merge_env_vars, method:test_merge_env_vars_tracks_overrides(self) → None, call:_merge_env_vars, method:test_merge_runtime_hints_basic(self) → None, call:_merge_runtime_hints, method:test_merge_files_basic(self) → None, call:_merge_files, method:test_merge_mounts_basic(self) → None, call:_merge_mounts, method:test_merge_mounts_file_override(self) → None, call:_merge_mounts, call:ResolvedMount, method:test_merge_mounts_mode_conflict(self) → None, call:_merge_mounts, call:ResolvedMount, method:test_merge_git_mounts_basic(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_concatenate_same_repo_branch(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_dedup_same_mapping(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_different_repos(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_different_branches(self) → None, call:_merge_git_mounts, call:len, call:m.get, class:TestResolveProfile, class:TestApplyResolvedProfile, method:test_mounts_directory_not_individual_files(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:Path(volumes[0]["source"]).is_dir, call:(Path(volumes[0]["source"]) / "config.json").exists, call:(Path(volumes[0]["source"]) / "nested" / "file.txt").exists, method:test_directory_mount_target(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:Path, call:(Path(volumes[0]["source"]) / "z.json").exists, method:test_empty_mount_produces_no_volumes(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, method:test_home_expansion_in_directory_mount_target(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:(Path(volumes[0]["source"]) / "app.toml").exists, call:Path, method:test_readonly_mount_sets_readonly_flag(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:volumes[0].get, method:test_writable_mount_does_not_set_readonly_flag(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:volumes[0].get, class:TestCheckIncludeCycle | dep: uuid, pathlib, pytest, sqlalchemy.ext.asyncio, src.models.config.config_profile, src.services.config.config_profile_resolver
|
||||||
- test_docker_build.py | Unit tests for a Docker image build service that verifies Dockerfile creation, command structure, context file handling, path traversal prevention, and error handling. | exp: class:TestBuildImage | dep: subprocess, tempfile, pathlib, unittest.mock, pytest, src.services.build.docker_build
|
- test_docker_build.py | Unit tests for a Docker image build service that verifies Dockerfile creation, command structure, context file handling, path traversal prevention, and error handling. | exp: class:TestBuildImage | dep: subprocess, tempfile, pathlib, unittest.mock, pytest, src.services.build.docker_build
|
||||||
@@ -19,9 +18,8 @@ Unit test suite for the API application covering core services, utilities, and i
|
|||||||
- test_git_url_parser.py | Tests for git URL parsing utilities that extract base repository URLs, validate clone URLs, and parse various git URL formats across GitHub, GitLab, and Bitbucket. | exp: class:TestExtractBaseRepoUrl, method:test_github_tree_url(self), call:extract_base_repo_url, method:test_github_blob_url(self), call:extract_base_repo_url, method:test_github_pull_url(self), call:extract_base_repo_url, method:test_github_issues_url(self), call:extract_base_repo_url, method:test_github_valid_url(self), call:extract_base_repo_url, method:test_github_url_with_query_params(self), call:extract_base_repo_url, method:test_gitlab_tree_url(self), call:extract_base_repo_url, method:test_gitlab_blob_url(self), call:extract_base_repo_url, method:test_gitlab_merge_request_url(self), call:extract_base_repo_url, method:test_gitlab_valid_url(self), call:extract_base_repo_url, method:test_bitbucket_src_url(self), call:extract_base_repo_url, method:test_bitbucket_valid_url(self), call:extract_base_repo_url, method:test_ssh_url(self), call:extract_base_repo_url, method:test_ssh_url_without_git_suffix(self), call:extract_base_repo_url, method:test_invalid_url(self), call:extract_base_repo_url, method:test_empty_url(self), call:extract_base_repo_url, class:TestIsValidCloneUrl, method:test_valid_ssh_url(self), call:is_valid_clone_url, method:test_valid_https_url(self), call:is_valid_clone_url, method:test_browser_url(self), call:is_valid_clone_url, method:test_url_without_git_suffix(self), call:is_valid_clone_url, method:test_invalid_url(self), call:is_valid_clone_url, class:TestParseGitUrl, method:test_valid_git_url(self), call:parse_git_url, method:test_browser_url(self), call:parse_git_url, method:test_invalid_url(self), call:parse_git_url, method:test_empty_url(self), call:parse_git_url, method:test_ssh_url(self), call:parse_git_url | dep: src.utils.git_url_parser, pytest
|
- test_git_url_parser.py | Tests for git URL parsing utilities that extract base repository URLs, validate clone URLs, and parse various git URL formats across GitHub, GitLab, and Bitbucket. | exp: class:TestExtractBaseRepoUrl, method:test_github_tree_url(self), call:extract_base_repo_url, method:test_github_blob_url(self), call:extract_base_repo_url, method:test_github_pull_url(self), call:extract_base_repo_url, method:test_github_issues_url(self), call:extract_base_repo_url, method:test_github_valid_url(self), call:extract_base_repo_url, method:test_github_url_with_query_params(self), call:extract_base_repo_url, method:test_gitlab_tree_url(self), call:extract_base_repo_url, method:test_gitlab_blob_url(self), call:extract_base_repo_url, method:test_gitlab_merge_request_url(self), call:extract_base_repo_url, method:test_gitlab_valid_url(self), call:extract_base_repo_url, method:test_bitbucket_src_url(self), call:extract_base_repo_url, method:test_bitbucket_valid_url(self), call:extract_base_repo_url, method:test_ssh_url(self), call:extract_base_repo_url, method:test_ssh_url_without_git_suffix(self), call:extract_base_repo_url, method:test_invalid_url(self), call:extract_base_repo_url, method:test_empty_url(self), call:extract_base_repo_url, class:TestIsValidCloneUrl, method:test_valid_ssh_url(self), call:is_valid_clone_url, method:test_valid_https_url(self), call:is_valid_clone_url, method:test_browser_url(self), call:is_valid_clone_url, method:test_url_without_git_suffix(self), call:is_valid_clone_url, method:test_invalid_url(self), call:is_valid_clone_url, class:TestParseGitUrl, method:test_valid_git_url(self), call:parse_git_url, method:test_browser_url(self), call:parse_git_url, method:test_invalid_url(self), call:parse_git_url, method:test_empty_url(self), call:parse_git_url, method:test_ssh_url(self), call:parse_git_url | dep: src.utils.git_url_parser, pytest
|
||||||
- test_health_monitor.py | Unit tests for HealthMonitor state-transition logic covering container crash detection, tunnel failure detection, recovery detection, write deduplication, exception resilience, and start/stop lifecycle. | exp: func:event_bus() → InstanceEventBus, call:InstanceEventBus, call:bus._reset_for_testing, func:health_monitor(event_bus: InstanceEventBus) → HealthMonitor, call:HealthMonitor, func:_create_running_instance(db_session) → ToolInstance, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, func:test_detects_container_crash(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_detects_tunnel_failure(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_detects_recovery(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:db_session.commit, call:HealthSnapshot, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_skips_writes_when_no_state_change(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:HealthSnapshot, call:patch, call:health_monitor._check_instance, call:db_session.execute, call:select(HealthCheck).where, call:len, call:result.scalars().all, func:test_docker_exception_resilience(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:RuntimeError, call:health_monitor._check_instance, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one_or_none, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_monitor_start_stop(health_monitor: HealthMonitor) → None, call:health_monitor.start, call:task.done, call:health_monitor.stop, call:suppress, call:task.cancelled | dep: asyncio, uuid, contextlib, unittest.mock, pytest, sqlalchemy, src.models.system.health_check, src.models.tool.tool_instance, src.models.user.user, src.services.instance.event_bus, src.services.instance.health_monitor
|
- test_health_monitor.py | Unit tests for HealthMonitor state-transition logic covering container crash detection, tunnel failure detection, recovery detection, write deduplication, exception resilience, and start/stop lifecycle. | exp: func:event_bus() → InstanceEventBus, call:InstanceEventBus, call:bus._reset_for_testing, func:health_monitor(event_bus: InstanceEventBus) → HealthMonitor, call:HealthMonitor, func:_create_running_instance(db_session) → ToolInstance, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, func:test_detects_container_crash(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_detects_tunnel_failure(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_detects_recovery(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:db_session.commit, call:HealthSnapshot, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_skips_writes_when_no_state_change(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:HealthSnapshot, call:patch, call:health_monitor._check_instance, call:db_session.execute, call:select(HealthCheck).where, call:len, call:result.scalars().all, func:test_docker_exception_resilience(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:RuntimeError, call:health_monitor._check_instance, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one_or_none, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_monitor_start_stop(health_monitor: HealthMonitor) → None, call:health_monitor.start, call:task.done, call:health_monitor.stop, call:suppress, call:task.cancelled | dep: asyncio, uuid, contextlib, unittest.mock, pytest, sqlalchemy, src.models.system.health_check, src.models.tool.tool_instance, src.models.user.user, src.services.instance.event_bus, src.services.instance.health_monitor
|
||||||
- test_home_path_expansion.py | Unit tests for home directory path expansion (~ and $HOME) in container paths and manifest home directory resolution. | exp: class:TestExpandContainerPath, method:test_tilde_slash_expands(self) → None, call:expand_container_path, method:test_tilde_alone_expands(self) → None, call:expand_container_path, method:test_dollar_home_slash_expands(self) → None, call:expand_container_path, method:test_dollar_home_alone_expands(self) → None, call:expand_container_path, method:test_absolute_path_unchanged(self) → None, call:expand_container_path, method:test_relative_path_unchanged(self) → None, call:expand_container_path, method:test_tilde_in_middle_unchanged(self) → None, call:expand_container_path, method:test_dollar_home_in_middle_unchanged(self) → None, call:expand_container_path, method:test_root_home(self) → None, call:expand_container_path, class:TestGetManifestHomeDir, method:test_with_user_block(self) → None, call:get_manifest_home_dir, method:test_without_user_block(self) → None, call:get_manifest_home_dir, method:test_with_empty_user_name(self) → None, call:get_manifest_home_dir, method:test_with_none_user_name(self) → None, call:get_manifest_home_dir | dep: pytest, src.services.config.config_profile_resolver, src.services.build.manifest_compiler
|
- test_home_path_expansion.py | Unit tests for home directory path expansion (~ and $HOME) in container paths and manifest home directory resolution. | exp: class:TestExpandContainerPath, method:test_tilde_slash_expands(self) → None, call:expand_container_path, method:test_tilde_alone_expands(self) → None, call:expand_container_path, method:test_dollar_home_slash_expands(self) → None, call:expand_container_path, method:test_dollar_home_alone_expands(self) → None, call:expand_container_path, method:test_absolute_path_unchanged(self) → None, call:expand_container_path, method:test_relative_path_unchanged(self) → None, call:expand_container_path, method:test_tilde_in_middle_unchanged(self) → None, call:expand_container_path, method:test_dollar_home_in_middle_unchanged(self) → None, call:expand_container_path, method:test_root_home(self) → None, call:expand_container_path, class:TestGetManifestHomeDir, method:test_with_user_block(self) → None, call:get_manifest_home_dir, method:test_without_user_block(self) → None, call:get_manifest_home_dir, method:test_with_empty_user_name(self) → None, call:get_manifest_home_dir, method:test_with_none_user_name(self) → None, call:get_manifest_home_dir | dep: pytest, src.services.config.config_profile_resolver, src.services.build.manifest_compiler
|
||||||
- test_instance_service.py | Unit tests for home directory expansion in docker-compose file modifications. | exp: class:TestModifyComposeFile, method:test_extra_volumes_expand_home_dir(self, tmp_path), call:compose_path.write_text, call:modify_compose_file, call:str, call:compose_path.read_text, method:test_working_directory_expands_home_dir(self, tmp_path), call:compose_path.write_text, call:modify_compose_file, call:str, call:compose_path.read_text | dep: pytest, src.services.tool.instance_service
|
|
||||||
- test_lifecycle_hooks.py | Unit tests for lifecycle hook helper functions that derive notification titles and determine whether events should trigger notifications. | exp: class:TestDeriveTitle, method:test_known_event_types(self) → None, call:_derive_title, method:test_unknown_event_type(self) → None, call:_derive_title, class:TestShouldNotify, method:test_error_events_are_notified(self) → None, call:_should_notify, method:test_health_changed_running_is_notified(self) → None, call:_should_notify, method:test_created_started_stopped_restarted_deleted_filtered(self) → None, call:_should_notify, method:test_health_changed_non_running_filtered(self) → None, call:_should_notify | dep: pytest, src.services.instance.lifecycle_hooks
|
- test_lifecycle_hooks.py | Unit tests for lifecycle hook helper functions that derive notification titles and determine whether events should trigger notifications. | exp: class:TestDeriveTitle, method:test_known_event_types(self) → None, call:_derive_title, method:test_unknown_event_type(self) → None, call:_derive_title, class:TestShouldNotify, method:test_error_events_are_notified(self) → None, call:_should_notify, method:test_health_changed_running_is_notified(self) → None, call:_should_notify, method:test_created_started_stopped_restarted_deleted_filtered(self) → None, call:_should_notify, method:test_health_changed_non_running_filtered(self) → None, call:_should_notify | dep: pytest, src.services.instance.lifecycle_hooks
|
||||||
- test_manifest_compiler.py | Unit tests for a manifest compiler that generates Docker configurations (Dockerfile, compose, entrypoint) based on manifest specifications. | exp: class:TestGetManifestHomeDir, method:test_home_directory_in_manifest_wins(self) → None, call:get_manifest_home_dir, method:test_user_name_derives_home(self) → None, call:get_manifest_home_dir, method:test_root_fallback(self) → None, call:get_manifest_home_dir, method:test_empty_home_directory_falls_back(self) → None, call:get_manifest_home_dir, class:TestCompileDockerfileHomeDirectory, method:test_env_home_and_workdir_use_home_directory(self) → None, call:compile_dockerfile, method:test_workspace_symlink_created(self) → None, call:compile_dockerfile, method:test_runtime_working_dir_overrides_home_workdir(self) → None, call:compile_dockerfile, method:test_working_dir_expands_tilde(self) → None, call:compile_dockerfile, class:TestCompileComposeHomeDirectory, method:test_default_repo_mount_synthesized(self) → None, call:compile_compose, method:test_explicit_repo_mount_preserved(self) → None, call:compile_compose, method:test_working_dir_expands_home(self) → None, call:compile_compose, class:TestCompileEntrypoint, method:test_entrypoint_creates_home_and_workspace(self) → None, call:compile_entrypoint, method:test_entrypoint_fixes_mount_owners(self) → None, call:compile_entrypoint, func:test_compile_dockerfile_creates_config_dirs_for_user() → None, call:compile_dockerfile, func:test_compile_dockerfile_no_user_does_not_create_home() → None, call:compile_dockerfile | dep: pytest, src.services.build.manifest_compiler
|
- test_manifest_compiler.py | Unit tests for a manifest compiler that generates Dockerfiles with user configuration and home directory setup | exp: func:test_compile_dockerfile_creates_config_dirs_for_user() → None, call:compile_dockerfile, func:test_compile_dockerfile_no_user_does_not_create_home() → None, call:compile_dockerfile | dep: pytest, src.services.build.manifest_compiler
|
||||||
- test_migration_metadata.py | Tests Alembic database migration files for correct table definitions and revision chain metadata | exp: func:test_initial_migration_defines_all_core_tables() → None, call:Path(__file__).resolve, call:spec_from_file_location, call:module_from_spec, call:spec.loader.exec_module, func:test_refresh_tokens_migration_has_expected_revision_chain() → None, call:Path(__file__).resolve, call:spec_from_file_location, call:module_from_spec, call:spec.loader.exec_module | dep: pytest, importlib.util, pathlib, pathlib.Path
|
- test_migration_metadata.py | Tests Alembic database migration files for correct table definitions and revision chain metadata | exp: func:test_initial_migration_defines_all_core_tables() → None, call:Path(__file__).resolve, call:spec_from_file_location, call:module_from_spec, call:spec.loader.exec_module, func:test_refresh_tokens_migration_has_expected_revision_chain() → None, call:Path(__file__).resolve, call:spec_from_file_location, call:module_from_spec, call:spec.loader.exec_module | dep: pytest, importlib.util, pathlib, pathlib.Path
|
||||||
- test_monitoring_models.py | Unit tests verifying creation, persistence, and querying of monitoring models (InstanceEvent and HealthCheck) with database migration compatibility. | exp: func:test_instance_event_creation(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:InstanceEvent, call:db_session.refresh, call:isinstance, func:test_health_check_creation(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:HealthCheck, call:db_session.refresh, call:isinstance, func:test_instance_event_query_by_instance(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:InstanceEvent, call:db_session.execute, call:select(InstanceEvent).where, call:result.scalar_one | dep: uuid, datetime, pytest, sqlalchemy, src.models.system.health_check, src.models.system.instance_event, src.models.tool.tool_instance, src.models.user.user
|
- test_monitoring_models.py | Unit tests verifying creation, persistence, and querying of monitoring models (InstanceEvent and HealthCheck) with database migration compatibility. | exp: func:test_instance_event_creation(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:InstanceEvent, call:db_session.refresh, call:isinstance, func:test_health_check_creation(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:HealthCheck, call:db_session.refresh, call:isinstance, func:test_instance_event_query_by_instance(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:InstanceEvent, call:db_session.execute, call:select(InstanceEvent).where, call:result.scalar_one | dep: uuid, datetime, pytest, sqlalchemy, src.models.system.health_check, src.models.system.instance_event, src.models.tool.tool_instance, src.models.user.user
|
||||||
- test_notification_service.py | Unit tests for NotificationService covering CRUD operations, filtering, sorting, and ownership isolation. | exp: func:notification_service() → NotificationService, call:NotificationService, func:user_a(db_session: AsyncSession) → User, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, func:user_b(db_session: AsyncSession) → User, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, func:test_create_notification(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:uuid.uuid4, func:test_list_notifications_orders_by_created_at_desc(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:datetime.now, call:timedelta, call:db_session.commit, call:db_session.refresh, call:notification_service.list_notifications, func:test_list_notifications_excludes_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:notification_service.list_notifications, func:test_list_notifications_unread_only(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.mark_read, call:notification_service.list_notifications, func:test_get_unread_count(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_read, call:notification_service.get_unread_count, func:test_mark_read_sets_read_at(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.mark_read, func:test_mark_all_read_affects_all_unread(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_all_read, call:notification_service.get_unread_count, func:test_dismiss_sets_dismissed_at(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:db_session.execute, call:select(Notification).where, call:result.scalar_one, func:test_mark_read_wrong_owner_raises(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:notification_service.create_notification, call:pytest.raises, call:notification_service.mark_read, func:test_dismiss_wrong_owner_raises(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:notification_service.create_notification, call:pytest.raises, call:notification_service.dismiss, func:test_list_notifications_mute_categories(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.list_notifications, func:test_get_unread_count_excludes_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:notification_service.get_unread_count, func:test_dismiss_all_affects_all_non_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.dismiss_all, call:notification_service.list_notifications, func:test_dismiss_all_affects_only_caller(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:range, call:notification_service.create_notification, call:notification_service.dismiss_all, call:notification_service.list_notifications, func:test_mark_all_read_affects_only_caller(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_all_read, call:notification_service.get_unread_count | dep: uuid, datetime, pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.models.system.notification, src.models.user.user, src.services.shared.notification_service, NotificationService, Notification, User, AsyncSession
|
- test_notification_service.py | Unit tests for NotificationService covering CRUD operations, filtering, sorting, and ownership isolation. | exp: func:notification_service() → NotificationService, call:NotificationService, func:user_a(db_session: AsyncSession) → User, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, func:user_b(db_session: AsyncSession) → User, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, func:test_create_notification(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:uuid.uuid4, func:test_list_notifications_orders_by_created_at_desc(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:datetime.now, call:timedelta, call:db_session.commit, call:db_session.refresh, call:notification_service.list_notifications, func:test_list_notifications_excludes_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:notification_service.list_notifications, func:test_list_notifications_unread_only(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.mark_read, call:notification_service.list_notifications, func:test_get_unread_count(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_read, call:notification_service.get_unread_count, func:test_mark_read_sets_read_at(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.mark_read, func:test_mark_all_read_affects_all_unread(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_all_read, call:notification_service.get_unread_count, func:test_dismiss_sets_dismissed_at(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:db_session.execute, call:select(Notification).where, call:result.scalar_one, func:test_mark_read_wrong_owner_raises(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:notification_service.create_notification, call:pytest.raises, call:notification_service.mark_read, func:test_dismiss_wrong_owner_raises(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:notification_service.create_notification, call:pytest.raises, call:notification_service.dismiss, func:test_list_notifications_mute_categories(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.list_notifications, func:test_get_unread_count_excludes_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:notification_service.get_unread_count, func:test_dismiss_all_affects_all_non_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.dismiss_all, call:notification_service.list_notifications, func:test_dismiss_all_affects_only_caller(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:range, call:notification_service.create_notification, call:notification_service.dismiss_all, call:notification_service.list_notifications, func:test_mark_all_read_affects_only_caller(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_all_read, call:notification_service.get_unread_count | dep: uuid, datetime, pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.models.system.notification, src.models.user.user, src.services.shared.notification_service, NotificationService, Notification, User, AsyncSession
|
||||||
@@ -30,9 +28,9 @@ Unit test suite for the API application covering core services, utilities, and i
|
|||||||
- test_readiness_probe.py | Unit tests for a Docker container readiness probe service that executes commands via docker exec with retry logic. | exp: class:TestExecuteProbe, class:TestIntegrationScenarios | dep: unittest.mock, src.services.shared.readiness_probe, subprocess
|
- test_readiness_probe.py | Unit tests for a Docker container readiness probe service that executes commands via docker exec with retry logic. | exp: class:TestExecuteProbe, class:TestIntegrationScenarios | dep: unittest.mock, src.services.shared.readiness_probe, subprocess
|
||||||
- test_ssh_keys.py | Unit tests for SSH key preparation functionality including file creation, permissions, ownership, and error handling | exp: class:TestPrepareSshKeyFiles | dep: os, pathlib, unittest.mock, pytest, src.services.shared.ssh_keys
|
- test_ssh_keys.py | Unit tests for SSH key preparation functionality including file creation, permissions, ownership, and error handling | exp: class:TestPrepareSshKeyFiles | dep: os, pathlib, unittest.mock, pytest, src.services.shared.ssh_keys
|
||||||
## arch
|
## arch
|
||||||
pytest-based unit testing with heavy mocking of external dependencies (Docker, Git, subprocess, database) to test components in isolation without live services.
|
Standard Python unittest/pytest pattern with heavy mocking of external dependencies (subprocess, docker, filesystem) to test service layer logic in isolation, organized by functional domain with one test module per service component.
|
||||||
## tags
|
## tags
|
||||||
test, url, call:notification, git, home, call:, merge, call:db
|
test, url, call:notification, git, call:, call:db, merge, src
|
||||||
## symbols
|
## symbols
|
||||||
- TestMergeFunctions
|
- TestMergeFunctions
|
||||||
- TestResolveProfile
|
- TestResolveProfile
|
||||||
@@ -44,7 +42,7 @@ test, url, call:notification, git, home, call:, merge, call:db
|
|||||||
- TestSortVolumesBySpecificity
|
- TestSortVolumesBySpecificity
|
||||||
## workflows
|
## workflows
|
||||||
- change unit behavior
|
- change unit behavior
|
||||||
read: __init__.py, test_alembic_migrations.py, test_config.py
|
read: __init__.py, test_config.py, test_config_profile_resolver.py
|
||||||
- change unit config
|
- change unit config
|
||||||
read: test_config.py, test_config_profile_resolver.py
|
read: test_config.py, test_config_profile_resolver.py
|
||||||
## dirty
|
## dirty
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
"""Unit tests for Alembic migration structure/import.
|
|
||||||
|
|
||||||
Actual upgrade/downgrade round-trips require a PostgreSQL database, so these
|
|
||||||
tests verify that migrations are importable, have the expected identifiers,
|
|
||||||
and declare the expected dependencies.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import importlib.util
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_home_directory_migration_imports_and_rewrites() -> None:
|
|
||||||
migration_path = Path(__file__).parent.parent.parent / (
|
|
||||||
"alembic/versions/2026_06_14_104415_add_tool_type_home_directory.py"
|
|
||||||
)
|
|
||||||
assert migration_path.exists()
|
|
||||||
|
|
||||||
spec = importlib.util.spec_from_file_location("home_dir_migration", migration_path)
|
|
||||||
assert spec is not None and spec.loader is not None
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
|
|
||||||
assert module.revision == "2026_06_14_104415"
|
|
||||||
assert module.down_revision == "f3d2dc90ba3a"
|
|
||||||
assert callable(module.upgrade)
|
|
||||||
assert callable(module.downgrade)
|
|
||||||
assert module.OLD_WORKSPACE == "/workspace"
|
|
||||||
assert module.NEW_WORKSPACE == "/home/user/{{WORKSPACE_NAME}}"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_merge_migration_resolves_heads() -> None:
|
|
||||||
migration_path = Path(__file__).parent.parent.parent / (
|
|
||||||
"alembic/versions/fc8f1a20cbf6_merge_home_directory_and_pi_agent_mount_.py"
|
|
||||||
)
|
|
||||||
assert migration_path.exists()
|
|
||||||
|
|
||||||
spec = importlib.util.spec_from_file_location("merge_migration", migration_path)
|
|
||||||
assert spec is not None and spec.loader is not None
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
|
|
||||||
assert module.revision == "fc8f1a20cbf6"
|
|
||||||
assert "2026_06_14_104415" in module.down_revision
|
|
||||||
assert "8c6d1dbd4798" in module.down_revision
|
|
||||||
assert callable(module.upgrade)
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
"""Unit tests for the tool instance service."""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from src.services.tool.instance_service import modify_compose_file
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
class TestModifyComposeFile:
|
|
||||||
"""Tests for modify_compose_file home-directory expansion."""
|
|
||||||
|
|
||||||
def test_extra_volumes_expand_home_dir(self, tmp_path):
|
|
||||||
compose_path = tmp_path / "docker-compose.yml"
|
|
||||||
compose_path.write_text(
|
|
||||||
'services:\n app:\n image: test:latest\n volumes: []\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
modify_compose_file(
|
|
||||||
str(compose_path),
|
|
||||||
extra_volumes=[
|
|
||||||
{"source": "/host/config", "target": "~/.config", "type": "bind"},
|
|
||||||
{"source": "/host/code", "target": "$HOME/code", "type": "bind"},
|
|
||||||
],
|
|
||||||
home_dir="/home/user",
|
|
||||||
)
|
|
||||||
|
|
||||||
content = compose_path.read_text()
|
|
||||||
assert "/host/config:/home/user/.config" in content
|
|
||||||
assert "/host/code:/home/user/code" in content
|
|
||||||
|
|
||||||
def test_working_directory_expands_home_dir(self, tmp_path):
|
|
||||||
compose_path = tmp_path / "docker-compose.yml"
|
|
||||||
compose_path.write_text(
|
|
||||||
'services:\n app:\n image: test:latest\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
modify_compose_file(
|
|
||||||
str(compose_path),
|
|
||||||
working_directory="~/workspace",
|
|
||||||
home_dir="/home/user",
|
|
||||||
)
|
|
||||||
|
|
||||||
content = compose_path.read_text()
|
|
||||||
assert "working_dir: /home/user/workspace" in content
|
|
||||||
@@ -2,12 +2,7 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from src.services.build.manifest_compiler import (
|
from src.services.build.manifest_compiler import compile_dockerfile
|
||||||
compile_compose,
|
|
||||||
compile_dockerfile,
|
|
||||||
compile_entrypoint,
|
|
||||||
get_manifest_home_dir,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
@@ -41,185 +36,3 @@ def test_compile_dockerfile_no_user_does_not_create_home() -> None:
|
|||||||
|
|
||||||
assert "useradd" not in dockerfile
|
assert "useradd" not in dockerfile
|
||||||
assert "/home/" not in dockerfile
|
assert "/home/" not in dockerfile
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
class TestGetManifestHomeDir:
|
|
||||||
"""Tests for get_manifest_home_dir precedence."""
|
|
||||||
|
|
||||||
def test_home_directory_in_manifest_wins(self) -> None:
|
|
||||||
manifest = {
|
|
||||||
"home_directory": "/home/custom",
|
|
||||||
"user": {"name": "dev"},
|
|
||||||
}
|
|
||||||
assert get_manifest_home_dir(manifest) == "/home/custom"
|
|
||||||
|
|
||||||
def test_user_name_derives_home(self) -> None:
|
|
||||||
manifest = {"user": {"name": "dev", "uid": 1000, "gid": 1000}}
|
|
||||||
assert get_manifest_home_dir(manifest) == "/home/dev"
|
|
||||||
|
|
||||||
def test_root_fallback(self) -> None:
|
|
||||||
manifest = {"base_image": "ubuntu:24.04"}
|
|
||||||
assert get_manifest_home_dir(manifest) == "/root"
|
|
||||||
|
|
||||||
def test_empty_home_directory_falls_back(self) -> None:
|
|
||||||
manifest = {"home_directory": "", "user": {"name": "dev"}}
|
|
||||||
assert get_manifest_home_dir(manifest) == "/home/dev"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
class TestCompileDockerfileHomeDirectory:
|
|
||||||
"""Tests that compile_dockerfile honors manifest.home_directory."""
|
|
||||||
|
|
||||||
def test_env_home_and_workdir_use_home_directory(self) -> None:
|
|
||||||
manifest = {
|
|
||||||
"base_image": "ubuntu:24.04",
|
|
||||||
"interface_type": "terminal",
|
|
||||||
"home_directory": "/home/custom",
|
|
||||||
"user": {"name": "dev", "uid": 1000, "gid": 1000},
|
|
||||||
}
|
|
||||||
dockerfile = compile_dockerfile(manifest)
|
|
||||||
|
|
||||||
assert "ENV HOME=/home/custom" in dockerfile
|
|
||||||
assert "WORKDIR /home/custom" in dockerfile
|
|
||||||
|
|
||||||
def test_workspace_symlink_created(self) -> None:
|
|
||||||
manifest = {
|
|
||||||
"base_image": "ubuntu:24.04",
|
|
||||||
"interface_type": "terminal",
|
|
||||||
"home_directory": "/home/custom",
|
|
||||||
"user": {"name": "dev", "uid": 1000, "gid": 1000},
|
|
||||||
}
|
|
||||||
dockerfile = compile_dockerfile(manifest)
|
|
||||||
|
|
||||||
assert "ln -sfn /home/custom/{{WORKSPACE_NAME}} /workspace" in dockerfile
|
|
||||||
|
|
||||||
def test_runtime_working_dir_overrides_home_workdir(self) -> None:
|
|
||||||
manifest = {
|
|
||||||
"base_image": "ubuntu:24.04",
|
|
||||||
"interface_type": "terminal",
|
|
||||||
"home_directory": "/home/custom",
|
|
||||||
"user": {"name": "dev", "uid": 1000, "gid": 1000},
|
|
||||||
"runtime": {"working_dir": "/app/code"},
|
|
||||||
}
|
|
||||||
dockerfile = compile_dockerfile(manifest)
|
|
||||||
|
|
||||||
assert "WORKDIR /app/code" in dockerfile
|
|
||||||
assert "WORKDIR /home/custom" not in dockerfile
|
|
||||||
|
|
||||||
def test_working_dir_expands_tilde(self) -> None:
|
|
||||||
manifest = {
|
|
||||||
"base_image": "ubuntu:24.04",
|
|
||||||
"interface_type": "terminal",
|
|
||||||
"home_directory": "/home/custom",
|
|
||||||
"user": {"name": "dev", "uid": 1000, "gid": 1000},
|
|
||||||
"runtime": {"working_dir": "~/code"},
|
|
||||||
}
|
|
||||||
dockerfile = compile_dockerfile(manifest)
|
|
||||||
|
|
||||||
assert "WORKDIR /home/custom/code" in dockerfile
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
class TestCompileComposeHomeDirectory:
|
|
||||||
"""Tests that compile_compose uses home_directory for volumes and working_dir."""
|
|
||||||
|
|
||||||
def test_default_repo_mount_synthesized(self) -> None:
|
|
||||||
manifest = {
|
|
||||||
"base_image": "ubuntu:24.04",
|
|
||||||
"interface_type": "terminal",
|
|
||||||
"home_directory": "/home/custom",
|
|
||||||
"user": {"name": "dev", "uid": 1000, "gid": 1000},
|
|
||||||
}
|
|
||||||
variables = {
|
|
||||||
"IMAGE_TAG": "test:latest",
|
|
||||||
"INSTANCE_NAME": "test-instance",
|
|
||||||
"REPO_PATH": "/host/repos/my-app",
|
|
||||||
"WORKSPACE_NAME": "my-app",
|
|
||||||
"TOOL_PORT": 0,
|
|
||||||
"EXTRA_ENV": {},
|
|
||||||
"EXTRA_VOLUMES": [],
|
|
||||||
}
|
|
||||||
compose = compile_compose(manifest, variables)
|
|
||||||
|
|
||||||
assert "/host/repos/my-app:/home/custom/my-app" in compose
|
|
||||||
|
|
||||||
def test_explicit_repo_mount_preserved(self) -> None:
|
|
||||||
manifest = {
|
|
||||||
"base_image": "ubuntu:24.04",
|
|
||||||
"interface_type": "terminal",
|
|
||||||
"home_directory": "/home/custom",
|
|
||||||
"user": {"name": "dev", "uid": 1000, "gid": 1000},
|
|
||||||
"mounts": [
|
|
||||||
{"source_type": "repo", "target": "/opt/code", "readonly": True}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
variables = {
|
|
||||||
"IMAGE_TAG": "test:latest",
|
|
||||||
"INSTANCE_NAME": "test-instance",
|
|
||||||
"REPO_PATH": "/host/repos/my-app",
|
|
||||||
"WORKSPACE_NAME": "my-app",
|
|
||||||
"TOOL_PORT": 0,
|
|
||||||
"EXTRA_ENV": {},
|
|
||||||
"EXTRA_VOLUMES": [],
|
|
||||||
}
|
|
||||||
compose = compile_compose(manifest, variables)
|
|
||||||
|
|
||||||
assert "/host/repos/my-app:/opt/code:ro" in compose
|
|
||||||
assert "/home/custom/my-app" not in compose
|
|
||||||
|
|
||||||
def test_working_dir_expands_home(self) -> None:
|
|
||||||
manifest = {
|
|
||||||
"base_image": "ubuntu:24.04",
|
|
||||||
"interface_type": "terminal",
|
|
||||||
"home_directory": "/home/custom",
|
|
||||||
"user": {"name": "dev", "uid": 1000, "gid": 1000},
|
|
||||||
"runtime": {"working_dir": "$HOME/code"},
|
|
||||||
}
|
|
||||||
variables = {
|
|
||||||
"IMAGE_TAG": "test:latest",
|
|
||||||
"INSTANCE_NAME": "test-instance",
|
|
||||||
"REPO_PATH": "/host/repos/my-app",
|
|
||||||
"WORKSPACE_NAME": "my-app",
|
|
||||||
"TOOL_PORT": 0,
|
|
||||||
"EXTRA_ENV": {},
|
|
||||||
"EXTRA_VOLUMES": [],
|
|
||||||
}
|
|
||||||
compose = compile_compose(manifest, variables)
|
|
||||||
|
|
||||||
assert "working_dir: /home/custom/code" in compose
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
class TestCompileEntrypoint:
|
|
||||||
"""Tests for the generated permission-fixing entrypoint."""
|
|
||||||
|
|
||||||
def test_entrypoint_creates_home_and_workspace(self) -> None:
|
|
||||||
manifest = {
|
|
||||||
"base_image": "ubuntu:24.04",
|
|
||||||
"interface_type": "terminal",
|
|
||||||
"home_directory": "/home/custom",
|
|
||||||
"user": {"name": "dev", "uid": 1000, "gid": 1000},
|
|
||||||
}
|
|
||||||
entrypoint = compile_entrypoint(manifest)
|
|
||||||
|
|
||||||
assert 'mkdir -p "$HOME_DIR"' in entrypoint
|
|
||||||
assert 'mkdir -p "$WORKSPACE_TARGET"' in entrypoint
|
|
||||||
assert 'ln -sfn "$WORKSPACE_TARGET" /workspace' in entrypoint
|
|
||||||
|
|
||||||
def test_entrypoint_fixes_mount_owners(self) -> None:
|
|
||||||
manifest = {
|
|
||||||
"base_image": "ubuntu:24.04",
|
|
||||||
"interface_type": "terminal",
|
|
||||||
"home_directory": "/home/custom",
|
|
||||||
"user": {"name": "dev", "uid": 1000, "gid": 1000},
|
|
||||||
"mounts": [
|
|
||||||
{"target": "~/.config", "readonly": False},
|
|
||||||
{"target": "/opt/readonly", "readonly": True},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
entrypoint = compile_entrypoint(manifest)
|
|
||||||
|
|
||||||
assert 'fix_owner "/home/custom/.config"' in entrypoint
|
|
||||||
assert 'fix_owner "/opt/readonly"' not in entrypoint
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web
|
dir: apps/web
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Frontend web application providing a browser-based UI with code editing, terminal, and routing capabilities for the "headquarter" project.
|
Frontend web application providing a React-based UI with code editing, terminal, and routing capabilities for the "headquarter" project.
|
||||||
## parent
|
## parent
|
||||||
index: apps/.pi-map.index.md
|
index: apps/.pi-map.index.md
|
||||||
map: apps/.pi-map.md
|
map: apps/.pi-map.md
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ dir: apps/web
|
|||||||
index: apps/web/.pi-map.index.md
|
index: apps/web/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Frontend web application providing a browser-based UI with code editing, terminal, and routing capabilities for the "headquarter" project.
|
Frontend web application providing a React-based UI with code editing, terminal, and routing capabilities for the "headquarter" project.
|
||||||
## files
|
## files
|
||||||
- .env.example | Template file defining example environment variables for frontend API and application URL configuration
|
- .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
|
- .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 providing a browser-based UI with code editing, termina
|
|||||||
- tsconfig.json | TypeScript configuration file for a React project using Vite with modern ES2020 target and bundler module resolution | dep: typescript, react, vite
|
- 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
|
- 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
|
## arch
|
||||||
Modern React SPA built with Vite and TypeScript, containerized via multi-stage Docker with nginx serving, featuring client-side routing, optimized static asset delivery, and development tooling (ESLint, Vitest).
|
Modern React SPA built with Vite and TypeScript, using nginx for production serving with client-side routing, multi-stage Docker deployment, and Vitest for testing.
|
||||||
## tags
|
## tags
|
||||||
react, eslint, vite, typescript, dom, application, nginx, web
|
react, eslint, vite, typescript, dom, application, nginx, web
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src
|
dir: apps/web/src
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides the core web application entry point, routing infrastructure, and shared domain type definitions for a React-based frontend.
|
Frontend web application entry point and core infrastructure for a React-based collaborative development platform.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/.pi-map.index.md
|
index: apps/web/.pi-map.index.md
|
||||||
map: apps/web/.pi-map.md
|
map: apps/web/.pi-map.md
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ dir: apps/web/src
|
|||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides the core web application entry point, routing infrastructure, and shared domain type definitions for a React-based frontend.
|
Frontend web application entry point and core infrastructure for a React-based collaborative development platform.
|
||||||
## files
|
## 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
|
- 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
|
- 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
|
- types.ts | Defines TypeScript type definitions for user sessions, projects, repositories, and workspaces in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos
|
||||||
## arch
|
## arch
|
||||||
Modular React SPA with provider-based dependency injection, declarative routing with nested layouts and route guards, and centralized TypeScript type definitions.
|
Modular React SPA using React Router v6 with nested route layouts, protected route guards via authentication context, and centralized TypeScript domain models for session/workspace/project entities.
|
||||||
## tags
|
## tags
|
||||||
pages, styles, css, router, react, session, dom, project
|
pages, styles, css, router, react, session, dom, project
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components
|
dir: apps/web/src/components
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides reusable, accessible UI primitives and layout components for a React web application, including shell layout, data states, icons, code editing, routing guards, and toast notifications.
|
Provides reusable, accessible UI components and utilities for rendering the application shell, data states, icons, code display, notifications, and route protection in a React web application.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
map: apps/web/src/.pi-map.md
|
map: apps/web/src/.pi-map.md
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ dir: apps/web/src/components
|
|||||||
index: apps/web/src/components/.pi-map.index.md
|
index: apps/web/src/components/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides reusable, accessible UI primitives and layout components for a React web application, including shell layout, data states, icons, code editing, routing guards, and toast notifications.
|
Provides reusable, accessible UI components and utilities for rendering the application shell, data states, icons, code display, notifications, and route protection in a React web application.
|
||||||
## files
|
## files
|
||||||
- app-shell.tsx | Renders the main application shell layout with navigation, header, session management, and mobile-responsive behavior for a React Router-based SPA. | 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, ./features/notification/event-toast-bridge, ./features/notification/notification-center, ./icon, ./features/mobile/mobile-nav, ./features/tool/start-tool-fab, ../utils/icons
|
- app-shell.tsx | Renders the main application shell layout with navigation, header, session management, and mobile-responsive behavior for a React Router-based SPA. | 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, ./features/notification/event-toast-bridge, ./features/notification/notification-center, ./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
|
- 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
|
||||||
@@ -17,7 +17,7 @@ Provides reusable, accessible UI primitives and layout components for a React we
|
|||||||
- toast-rules.test.ts | Unit tests for mapping instance events to toast notification categories and severities | dep: vitest, ./toast-rules, ../types/events
|
- 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
|
- 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
|
## arch
|
||||||
Component-based React architecture with functional components, composition patterns, and separation of concerns between presentation (UI rendering), logic (rules/hooks), and testing; integrates third-party libraries (react-simple-code-editor, Phosphor icons) and React Router for SPA navigation.
|
Component-based React architecture with functional components, composition patterns, separation of concerns (presentation vs logic), and test coverage for critical utilities; integrates third-party libraries (Phosphor icons, react-simple-code-editor) and React Router for SPA navigation.
|
||||||
## tags
|
## tags
|
||||||
toast, icon, react, state, code, loading, event, editor
|
toast, icon, react, state, code, loading, event, editor
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features
|
dir: apps/web/src/components/features
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains reusable React components that implement specific product features and user-facing functionality across the web application.
|
Contains reusable React components that implement specific user-facing features and functionality across the web application.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/components/.pi-map.index.md
|
index: apps/web/src/components/.pi-map.index.md
|
||||||
map: apps/web/src/components/.pi-map.md
|
map: apps/web/src/components/.pi-map.md
|
||||||
@@ -19,9 +19,6 @@ map: apps/web/src/components/.pi-map.md
|
|||||||
- apps/web/src/components/features/notification
|
- apps/web/src/components/features/notification
|
||||||
index: apps/web/src/components/features/notification/.pi-map.index.md
|
index: apps/web/src/components/features/notification/.pi-map.index.md
|
||||||
map: apps/web/src/components/features/notification/.pi-map.md
|
map: apps/web/src/components/features/notification/.pi-map.md
|
||||||
- apps/web/src/components/features/profile
|
|
||||||
index: apps/web/src/components/features/profile/.pi-map.index.md
|
|
||||||
map: apps/web/src/components/features/profile/.pi-map.md
|
|
||||||
- apps/web/src/components/features/project
|
- apps/web/src/components/features/project
|
||||||
index: apps/web/src/components/features/project/.pi-map.index.md
|
index: apps/web/src/components/features/project/.pi-map.index.md
|
||||||
map: apps/web/src/components/features/project/.pi-map.md
|
map: apps/web/src/components/features/project/.pi-map.md
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ dir: apps/web/src/components/features
|
|||||||
index: apps/web/src/components/features/.pi-map.index.md
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains reusable React components that implement specific product features and user-facing functionality across the web application.
|
Contains reusable React components that implement specific user-facing features and functionality across the web application.
|
||||||
## files
|
## files
|
||||||
## arch
|
## arch
|
||||||
Feature-based component organization with domain-specific groupings, likely combining presentational and container patterns with hooks for state management, following a modular architecture where each feature encapsulates its own UI, logic, and data fetching concerns.
|
Feature-based component organization with domain-specific UI building blocks following React composition patterns, likely co-located with related hooks, utilities, or sub-components for each feature area.
|
||||||
## tags
|
## tags
|
||||||
-
|
-
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features/config-profiles
|
dir: apps/web/src/components/features/config-profiles
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides UI components for managing Docker configuration profiles including creation, editing, listing, and mobile-responsive views.
|
Provides a complete UI subsystem for managing configuration profiles with desktop and mobile views, supporting CRUD operations, profile relationships, and advanced configuration options.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/components/features/.pi-map.index.md
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
map: apps/web/src/components/features/.pi-map.md
|
map: apps/web/src/components/features/.pi-map.md
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ dir: apps/web/src/components/features/config-profiles
|
|||||||
index: apps/web/src/components/features/config-profiles/.pi-map.index.md
|
index: apps/web/src/components/features/config-profiles/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides UI components for managing Docker configuration profiles including creation, editing, listing, and mobile-responsive views.
|
Provides a complete UI subsystem for managing configuration profiles with desktop and mobile views, supporting CRUD operations, profile relationships, and advanced configuration options.
|
||||||
## files
|
## files
|
||||||
- ConfigProfileEditorPanel.tsx | React component that renders a form-based editor panel for creating and editing configuration profiles with support for includes, environment variables, runtime hints, files, and mounts. | exp: ConfigProfileEditorPanel | dep: ../../icon, ../git/git-mount-editor, ../../../api/config-profiles, ../../../types, ../../../api/tool-types, React, Icon, GitMountEditor, ConfigProfile, CreateConfigProfileRequest, ResolvedProfile, ProjectWithRepos, ToolType
|
- ConfigProfileEditorPanel.tsx | React component that renders a form-based editor panel for creating and editing configuration profiles with support for includes, environment variables, runtime hints, files, and mounts. | exp: ConfigProfileEditorPanel | dep: ../../icon, ../git/git-mount-editor, ../../../api/config-profiles, ../../../types, ../../../api/tool-types, React, Icon, GitMountEditor, ConfigProfile, CreateConfigProfileRequest, ResolvedProfile, ProjectWithRepos, ToolType
|
||||||
- ConfigProfileListSidebar.tsx | Renders a sidebar component for listing, selecting, creating, and deleting configuration profiles with visual indicators for default status, scope, and includes. | exp: ConfigProfileListSidebar | dep: ../../icon, ../../../api/config-profiles, Icon, ConfigProfile
|
- ConfigProfileListSidebar.tsx | Renders a sidebar component for listing, selecting, creating, and deleting configuration profiles with visual indicators for default status, scope, and includes. | exp: ConfigProfileListSidebar | dep: ../../icon, ../../../api/config-profiles, Icon, ConfigProfile
|
||||||
- ConfigProfilesMobileView.tsx | Renders a mobile-responsive view for managing configuration profiles with list, detail, edit, and preview states. | exp: ConfigProfilesMobileView | dep: react, ../mobile/mobile-list-view, ../mobile/mobile-detail-view, ../mobile/mobile-edit-view, ../mobile/mobile-fab, ../git/git-mount-editor, ../../icon, ../../../api/config-profiles, ../../../types, ../../../api/tool-types, mobile-list-view, mobile-detail-view, mobile-edit-view, mobile-fab, git-mount-editor, icon, config-profiles API types, types
|
- ConfigProfilesMobileView.tsx | Renders a mobile-responsive CRUD interface for managing configuration profiles with list, detail, and edit views. | exp: ConfigProfilesMobileView | dep: ../mobile/mobile-list-view, ../mobile/mobile-detail-view, ../mobile/mobile-edit-view, ../mobile/mobile-fab, ../../icon, ../../../api/config-profiles, MobileListView, MobileDetailView, MobileEditView, MobileFAB, Icon, ConfigProfile, CreateConfigProfileRequest
|
||||||
## arch
|
## arch
|
||||||
Feature-based component composition with state-driven panels (list/detail/edit/preview), form-based editing, and responsive layout adaptation for mobile/desktop.
|
Compound component architecture with panel/sidebar split for desktop, dedicated mobile view with state-driven routing, shared state management across list/editor views, and form-based configuration with nested array/object handling for includes, env vars, files, and mounts.
|
||||||
## tags
|
## tags
|
||||||
mobile, config, view, profiles, editor, profile, list, icon
|
config, mobile, profiles, profile, view, editor, icon, list
|
||||||
## symbols
|
## symbols
|
||||||
- ConfigProfileEditorPanel
|
- ConfigProfileEditorPanel
|
||||||
- ConfigProfileListSidebar
|
- ConfigProfileListSidebar
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features/mobile
|
dir: apps/web/src/components/features/mobile
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides a complete set of mobile-optimized UI components for CRUD operations, navigation, and terminal interfaces in a responsive web application.
|
Provides mobile-optimized UI components for core application features including navigation, data views, forms, modals, and specialized terminal interfaces.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/components/features/.pi-map.index.md
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
map: apps/web/src/components/features/.pi-map.md
|
map: apps/web/src/components/features/.pi-map.md
|
||||||
|
|||||||
@@ -4,22 +4,22 @@ dir: apps/web/src/components/features/mobile
|
|||||||
index: apps/web/src/components/features/mobile/.pi-map.index.md
|
index: apps/web/src/components/features/mobile/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides a complete set of mobile-optimized UI components for CRUD operations, navigation, and terminal interfaces in a responsive web application.
|
Provides mobile-optimized UI components for core application features including navigation, data views, forms, modals, and specialized terminal interfaces.
|
||||||
## files
|
## files
|
||||||
- mobile-action-sheet.tsx | Renders a mobile-optimized action sheet modal with title, configurable action buttons, and cancel option. | exp: MobileActionSheetItem, func:MobileActionSheet({ isOpen, onClose, title, actions, }: MobileActionSheetProps), call:useRef, call:useEffect, call:onClose, call:document.addEventListener, call:document.removeEventListener, call:e.stopPropagation, call:actions.map, call:action.onClick | dep: react, ../../icon, icon
|
- mobile-action-sheet.tsx | Renders a mobile-optimized action sheet modal with title, configurable action buttons, and cancel option. | exp: MobileActionSheetItem, func:MobileActionSheet({ isOpen, onClose, title, actions, }: MobileActionSheetProps), call:useRef, call:useEffect, call:onClose, call:document.addEventListener, call:document.removeEventListener, call:e.stopPropagation, call:actions.map, call:action.onClick | dep: react, ../../icon, icon
|
||||||
- mobile-detail-view.tsx | A React component that renders a mobile-optimized detail view with a header, back/edit/delete actions, and a configurable list of typed fields. | exp: MobileDetailView | dep: ../../icon, React
|
- mobile-detail-view.tsx | A React component that renders a mobile-optimized detail view with a header (back button, title, edit/delete actions) and a field list supporting multiple value types (text, code, JSON, boolean). | exp: MobileDetailView | dep: ../../icon, react
|
||||||
- mobile-edit-view.tsx | A React component that renders a mobile-optimized form for editing records with configurable field types and save/cancel/delete actions. | exp: MobileEditView | dep: react
|
- mobile-edit-view.tsx | A React component that renders a mobile-optimized form for editing data with configurable field types and save/cancel actions. | exp: MobileEditView | dep: react
|
||||||
- mobile-fab.tsx | Renders a floating action button component for mobile with an add icon and configurable click handler and label. | exp: MobileFAB | dep: ../../icon, react, icon
|
- mobile-fab.tsx | Renders a floating action button component for mobile with an add icon and configurable click handler and label. | exp: MobileFAB | dep: ../../icon, react, icon
|
||||||
- mobile-list-view.tsx | A reusable React component that renders a mobile-optimized list view with optional search, custom item rendering, and action buttons (duplicate/delete). | exp: MobileListView | dep: react, ../../icon, ../../../utils/icons, icon
|
- mobile-list-view.tsx | Renders a mobile-optimized list view with optional search, empty state, and customizable item rendering | exp: MobileListView | dep: react, ../../icon, ../../../utils/icons, icon
|
||||||
- mobile-nav.tsx | Renders a mobile navigation bar with grouped items that trigger bottom sheets and standard links with active state highlighting | exp: MobileNav | dep: react, react-router-dom, ../../icon, ../tool/tools-bottom-sheet, ./spaces-bottom-sheet, ../../../utils/icons
|
- mobile-nav.tsx | Renders a mobile navigation bar with grouped items that open bottom sheets and standard items that use React Router links, including active state indicators and session count badges. | exp: MobileNav | dep: react, react-router-dom, ../../icon, ../tool/tools-bottom-sheet, ./spaces-bottom-sheet, ../../../utils/icons
|
||||||
- mobile-page-header.tsx | Renders a mobile-only page header with optional back navigation and custom actions. | exp: func:MobilePageHeader({ title, showBack = true, actions }: MobilePageHeaderProps), call:useNavigate, call:useMobileViewport, call:navigate | dep: react-router-dom, ../../../hooks/use-mobile-viewport, ../../icon, use-mobile-viewport hook, Icon component
|
- mobile-page-header.tsx | Renders a mobile-only page header with optional back navigation and custom actions. | exp: func:MobilePageHeader({ title, showBack = true, actions }: MobilePageHeaderProps), call:useNavigate, call:useMobileViewport, call:navigate | dep: react-router-dom, ../../../hooks/use-mobile-viewport, ../../icon, use-mobile-viewport hook, Icon component
|
||||||
- mobile-terminal-header.tsx | Renders a mobile-responsive header for a terminal interface with navigation, title, connection status, font size controls, and close actions. | exp: MobileTerminalHeader | dep: react, ../../icon, icon
|
- mobile-terminal-header.tsx | Renders a mobile-responsive header for a terminal interface with navigation, title, connection status, font size controls, and close actions. | exp: MobileTerminalHeader | dep: react, ../../icon, icon
|
||||||
- mobile-terminal-wrapper.tsx | Wraps a terminal component with mobile-specific UI including auto-hiding header, virtual keyboard handling, and special keys interface. | exp: MobileTerminalWrapper | dep: react, ../terminal/terminal, ./mobile-terminal-header, ../terminal/special-keys-strip, ../terminal/special-keys-panel, ../../../hooks/use-mobile-viewport, ../../../hooks/use-virtual-keyboard, ../../../hooks/use-auto-hide, ../../../hooks/use-special-keys
|
- mobile-terminal-wrapper.tsx | Wraps a terminal component with mobile-specific UI including auto-hiding header, virtual keyboard handling, and special keys interface. | exp: MobileTerminalWrapper | dep: react, ../terminal/terminal, ./mobile-terminal-header, ../terminal/special-keys-strip, ../terminal/special-keys-panel, ../../../hooks/use-mobile-viewport, ../../../hooks/use-virtual-keyboard, ../../../hooks/use-auto-hide, ../../../hooks/use-special-keys
|
||||||
- spaces-bottom-sheet.tsx | Renders a mobile bottom sheet navigation menu for switching between "Projects" and "Workspaces" spaces with active state highlighting. | exp: SpacesBottomSheet | dep: react-router-dom, ../../icon, icon
|
- spaces-bottom-sheet.tsx | Renders a mobile bottom sheet navigation menu for switching between "Projects" and "Workspaces" spaces with active state highlighting. | exp: SpacesBottomSheet | dep: react-router-dom, ../../icon, icon
|
||||||
## arch
|
## arch
|
||||||
Feature-based component architecture using compound mobile patterns (bottom sheets, action sheets, FABs) with typed configurable props, consistent mobile-first design system, and specialized terminal wrapper with native-like behaviors (auto-hiding headers, virtual keyboard handling).
|
Compositional React component library with feature-specific mobile adaptations, using bottom sheets/action sheets for mobile-native UX patterns, and wrapper components that inject mobile-specific behaviors (auto-hiding headers, virtual keyboard handling) into existing features.
|
||||||
## tags
|
## tags
|
||||||
mobile, terminal, sheet, react, icon, view, header, action
|
mobile, terminal, sheet, react, icon, view, header, renders
|
||||||
## symbols
|
## symbols
|
||||||
- MobileActionSheet
|
- MobileActionSheet
|
||||||
- MobilePageHeader
|
- MobilePageHeader
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ interface MobileDetailViewProps {
|
|||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MobileDetailView: React.FC<MobileDetailViewProps> = ({
|
export const MobileDetailView: React.FC<MobileDetailViewProps> = ({
|
||||||
@@ -23,7 +22,6 @@ export const MobileDetailView: React.FC<MobileDetailViewProps> = ({
|
|||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onBack,
|
onBack,
|
||||||
children,
|
|
||||||
}) => {
|
}) => {
|
||||||
const renderValue = (field: Field) => {
|
const renderValue = (field: Field) => {
|
||||||
if (field.value === null || field.value === undefined) {
|
if (field.value === null || field.value === undefined) {
|
||||||
@@ -92,7 +90,6 @@ export const MobileDetailView: React.FC<MobileDetailViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{children}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ interface MobileEditViewProps {
|
|||||||
fields?: FormField[];
|
fields?: FormField[];
|
||||||
onSave: (data: Record<string, string | number | boolean>) => void;
|
onSave: (data: Record<string, string | number | boolean>) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onDelete?: () => void;
|
|
||||||
deleteLabel?: string;
|
|
||||||
isSaving?: boolean;
|
isSaving?: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
@@ -27,8 +25,6 @@ export const MobileEditView: React.FC<MobileEditViewProps> = ({
|
|||||||
fields,
|
fields,
|
||||||
onSave,
|
onSave,
|
||||||
onCancel,
|
onCancel,
|
||||||
onDelete,
|
|
||||||
deleteLabel = "Delete",
|
|
||||||
isSaving = false,
|
isSaving = false,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -63,7 +59,14 @@ export const MobileEditView: React.FC<MobileEditViewProps> = ({
|
|||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<h1 className="mobile-edit-title">{title}</h1>
|
<h1 className="mobile-edit-title">{title}</h1>
|
||||||
<div style={{ width: "4rem" }} />
|
<button
|
||||||
|
className="mobile-edit-save"
|
||||||
|
onClick={() => onSave(formData)}
|
||||||
|
type="button"
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
{isSaving ? "Saving..." : "Save"}
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<form className="mobile-edit-form" onSubmit={handleSubmit}>
|
<form className="mobile-edit-form" onSubmit={handleSubmit}>
|
||||||
@@ -153,27 +156,6 @@ export const MobileEditView: React.FC<MobileEditViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="mobile-edit-actions">
|
|
||||||
<button
|
|
||||||
className="mobile-edit-save"
|
|
||||||
onClick={() => onSave(formData)}
|
|
||||||
type="button"
|
|
||||||
disabled={isSaving}
|
|
||||||
>
|
|
||||||
{isSaving ? "Saving..." : "Save"}
|
|
||||||
</button>
|
|
||||||
{onDelete && (
|
|
||||||
<button
|
|
||||||
className="mobile-edit-delete"
|
|
||||||
onClick={onDelete}
|
|
||||||
type="button"
|
|
||||||
disabled={isSaving}
|
|
||||||
>
|
|
||||||
{deleteLabel}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ interface MobileListViewProps {
|
|||||||
export const MobileListView: React.FC<MobileListViewProps> = ({
|
export const MobileListView: React.FC<MobileListViewProps> = ({
|
||||||
items,
|
items,
|
||||||
onItemClick,
|
onItemClick,
|
||||||
onItemDelete,
|
|
||||||
onItemDuplicate,
|
|
||||||
emptyMessage = "No items found",
|
emptyMessage = "No items found",
|
||||||
searchPlaceholder = "Search...",
|
searchPlaceholder = "Search...",
|
||||||
onSearch,
|
onSearch,
|
||||||
@@ -75,38 +73,11 @@ export const MobileListView: React.FC<MobileListViewProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mobile-list-item-actions">
|
<div
|
||||||
{onItemDuplicate && (
|
className="mobile-list-item-actions"
|
||||||
<button
|
style={{ transform: "rotate(180deg)" }}
|
||||||
type="button"
|
>
|
||||||
className="mobile-list-item-action mobile-list-item-action-duplicate"
|
<Icon name="arrow-left" size="sm" />
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onItemDuplicate(item.id);
|
|
||||||
}}
|
|
||||||
aria-label="Duplicate"
|
|
||||||
>
|
|
||||||
<Icon name="copy" size="sm" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{onItemDelete && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="mobile-list-item-action mobile-list-item-action-delete"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onItemDelete(item.id);
|
|
||||||
}}
|
|
||||||
aria-label="Delete"
|
|
||||||
>
|
|
||||||
<Icon name="delete" size="sm" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{!onItemDelete && !onItemDuplicate && (
|
|
||||||
<span style={{ transform: "rotate(180deg)" }}>
|
|
||||||
<Icon name="arrow-left" size="sm" />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
# apps/web/src/components/features/profile (index)
|
|
||||||
dir: apps/web/src/components/features/profile
|
|
||||||
|
|
||||||
## role
|
|
||||||
Provides a mobile-specific UI for users to view and edit their profile information.
|
|
||||||
## parent
|
|
||||||
index: apps/web/src/components/features/.pi-map.index.md
|
|
||||||
map: apps/web/src/components/features/.pi-map.md
|
|
||||||
## children
|
|
||||||
-
|
|
||||||
## files
|
|
||||||
- ProfileMobileView.tsx
|
|
||||||
## links
|
|
||||||
index: apps/web/src/components/features/profile/.pi-map.index.md
|
|
||||||
map: apps/web/src/components/features/profile/.pi-map.md
|
|
||||||
## workflows
|
|
||||||
- change profile behavior
|
|
||||||
read: ProfileMobileView.tsx
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# apps/web/src/components/features/profile
|
|
||||||
dir: apps/web/src/components/features/profile
|
|
||||||
|
|
||||||
index: apps/web/src/components/features/profile/.pi-map.index.md
|
|
||||||
|
|
||||||
## role
|
|
||||||
Provides a mobile-specific UI for users to view and edit their profile information.
|
|
||||||
## files
|
|
||||||
- ProfileMobileView.tsx | Renders a mobile-optimized profile editing form with avatar upload, name/email fields, and save/cancel functionality. | exp: ProfileMobileView | dep: react-router-dom, ../mobile/mobile-edit-view, ../../icon, ../../../api/profile
|
|
||||||
## arch
|
|
||||||
Single feature-focused component with form handling and file upload, likely using controlled inputs and local state management.
|
|
||||||
## tags
|
|
||||||
mobile, profile, view, renders, optimized, editing, form, avatar
|
|
||||||
## symbols
|
|
||||||
- ProfileMobileView
|
|
||||||
## workflows
|
|
||||||
- change profile behavior
|
|
||||||
read: ProfileMobileView.tsx
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { MobileEditView } from "../mobile/mobile-edit-view";
|
|
||||||
import { Icon } from "../../icon";
|
|
||||||
import type { UserProfile } from "../../../api/profile";
|
|
||||||
|
|
||||||
interface ProfileMobileViewProps {
|
|
||||||
profile: UserProfile;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
error: string | null;
|
|
||||||
isSaving: boolean;
|
|
||||||
avatarUrl: string | null;
|
|
||||||
fileInputRef: React.RefObject<HTMLInputElement>;
|
|
||||||
onNameChange: (value: string) => void;
|
|
||||||
onEmailChange: (value: string) => void;
|
|
||||||
onAvatarButtonClick: () => void;
|
|
||||||
onAvatarChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
||||||
onSave: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProfileMobileView = ({
|
|
||||||
profile,
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
error,
|
|
||||||
isSaving,
|
|
||||||
avatarUrl,
|
|
||||||
fileInputRef,
|
|
||||||
onNameChange,
|
|
||||||
onEmailChange,
|
|
||||||
onAvatarButtonClick,
|
|
||||||
onAvatarChange,
|
|
||||||
onSave,
|
|
||||||
}: ProfileMobileViewProps) => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MobileEditView
|
|
||||||
title="Profile"
|
|
||||||
onCancel={() => navigate(-1)}
|
|
||||||
onSave={onSave}
|
|
||||||
isSaving={isSaving}
|
|
||||||
>
|
|
||||||
<div className="profile-mobile-avatar-section">
|
|
||||||
<div className="profile-mobile-avatar">
|
|
||||||
{avatarUrl ? (
|
|
||||||
<img alt="Avatar" className="profile-mobile-avatar-image" src={avatarUrl} />
|
|
||||||
) : (
|
|
||||||
<div className="profile-mobile-avatar-placeholder">
|
|
||||||
{profile.name.charAt(0).toUpperCase()}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="secondary-button"
|
|
||||||
disabled={isSaving}
|
|
||||||
onClick={onAvatarButtonClick}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{isSaving ? (
|
|
||||||
<>
|
|
||||||
<Icon name="loading" size="sm" />
|
|
||||||
Uploading...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Icon name="edit" size="sm" />
|
|
||||||
Change Avatar
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
accept="image/png,image/jpeg"
|
|
||||||
onChange={onAvatarChange}
|
|
||||||
ref={fileInputRef}
|
|
||||||
style={{ display: "none" }}
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
|
||||||
<label className="mobile-form-label" htmlFor="profile-mobile-name">
|
|
||||||
Name *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
className="mobile-form-input"
|
|
||||||
disabled={isSaving}
|
|
||||||
id="profile-mobile-name"
|
|
||||||
onChange={(e) => onNameChange(e.target.value)}
|
|
||||||
placeholder="Your name"
|
|
||||||
type="text"
|
|
||||||
value={name}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
|
||||||
<label className="mobile-form-label" htmlFor="profile-mobile-email">
|
|
||||||
Email *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
className="mobile-form-input"
|
|
||||||
disabled={isSaving}
|
|
||||||
id="profile-mobile-email"
|
|
||||||
onChange={(e) => onEmailChange(e.target.value)}
|
|
||||||
placeholder="your.email@example.com"
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<p className="mobile-form-error">
|
|
||||||
<Icon name="warning" size="sm" />
|
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</MobileEditView>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -363,34 +363,32 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
let startY = 0;
|
let startY = 0;
|
||||||
let startX = 0;
|
let startX = 0;
|
||||||
let isScrolling = false;
|
let isScrolling = false;
|
||||||
let scrollPending = 0;
|
let debugOverlay: HTMLDivElement | null = null;
|
||||||
const WHEEL_DISTANCE = 12;
|
const showDebug = (msg: string) => {
|
||||||
|
if (!debugOverlay) {
|
||||||
const flushScroll = (force = false) => {
|
debugOverlay = document.createElement("div");
|
||||||
if (scrollPending === 0) return;
|
debugOverlay.style.position = "fixed";
|
||||||
if (!force && Math.abs(scrollPending) < WHEEL_DISTANCE) return;
|
debugOverlay.style.bottom = "120px";
|
||||||
const direction = Math.sign(scrollPending);
|
debugOverlay.style.left = "8px";
|
||||||
const steps = Math.max(
|
debugOverlay.style.right = "8px";
|
||||||
1,
|
debugOverlay.style.zIndex = "9999";
|
||||||
Math.floor(Math.abs(scrollPending) / WHEEL_DISTANCE),
|
debugOverlay.style.background = "rgba(0,0,0,0.8)";
|
||||||
);
|
debugOverlay.style.color = "#0f0";
|
||||||
const ws = wsRef.current;
|
debugOverlay.style.fontFamily = "monospace";
|
||||||
if (ws?.readyState === WebSocket.OPEN && termRef.current) {
|
debugOverlay.style.fontSize = "11px";
|
||||||
const buf = termRef.current.buffer.active;
|
debugOverlay.style.padding = "8px";
|
||||||
const col = buf.cursorX + 1;
|
debugOverlay.style.borderRadius = "6px";
|
||||||
const row = buf.cursorY + 1;
|
debugOverlay.style.pointerEvents = "none";
|
||||||
const btn = direction > 0 ? 64 : 65;
|
debugOverlay.style.whiteSpace = "pre-wrap";
|
||||||
for (let i = 0; i < steps; i++) {
|
debugOverlay.style.maxHeight = "120px";
|
||||||
ws.send(`\x1b[<${btn};${col};${row}M`);
|
debugOverlay.style.overflow = "auto";
|
||||||
}
|
document.body.appendChild(debugOverlay);
|
||||||
}
|
}
|
||||||
scrollPending = 0;
|
const line = `${new Date().toLocaleTimeString()} ${msg}`;
|
||||||
};
|
debugOverlay.textContent =
|
||||||
|
line +
|
||||||
const isAlternateScreen = () => {
|
"\n" +
|
||||||
const term = termRef.current;
|
debugOverlay.textContent?.split("\n").slice(0, 8).join("\n");
|
||||||
if (!term) return false;
|
|
||||||
return term.buffer.active === term.buffer.alternate;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTouchStart = (e: TouchEvent) => {
|
const onTouchStart = (e: TouchEvent) => {
|
||||||
@@ -398,7 +396,6 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
startY = e.touches[0].clientY;
|
startY = e.touches[0].clientY;
|
||||||
startX = e.touches[0].clientX;
|
startX = e.touches[0].clientX;
|
||||||
isScrolling = false;
|
isScrolling = false;
|
||||||
scrollPending = 0;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onTouchMove = (e: TouchEvent) => {
|
const onTouchMove = (e: TouchEvent) => {
|
||||||
@@ -406,6 +403,7 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
const touch = e.touches[0];
|
const touch = e.touches[0];
|
||||||
const deltaY = startY - touch.clientY;
|
const deltaY = startY - touch.clientY;
|
||||||
const deltaX = Math.abs(startX - touch.clientX);
|
const deltaX = Math.abs(startX - touch.clientX);
|
||||||
|
showDebug(`move dy=${deltaY.toFixed(0)} dx=${deltaX.toFixed(0)} scrolling=${isScrolling}`);
|
||||||
|
|
||||||
// Decide early whether this is a vertical scroll gesture.
|
// Decide early whether this is a vertical scroll gesture.
|
||||||
if (!isScrolling) {
|
if (!isScrolling) {
|
||||||
@@ -415,32 +413,34 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
}
|
}
|
||||||
if (!isScrolling) return;
|
if (!isScrolling) return;
|
||||||
|
|
||||||
// Take over the gesture so the page/toolbar never scrolls.
|
// Take over the gesture so the browser never scrolls the
|
||||||
|
// page or the terminal canvas. We synthesize wheel events
|
||||||
|
// and dispatch them into xterm.js, which already handles
|
||||||
|
// both normal buffer scrolling and alternate-screen apps
|
||||||
|
// (tmux/vim) correctly.
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (isAlternateScreen()) {
|
const target = container.querySelector(".xterm-viewport") as
|
||||||
// Alternate screen (tmux/vim): accumulate the swipe and
|
| HTMLElement
|
||||||
// send SGR 1006 mouse-wheel events in steps.
|
| null;
|
||||||
scrollPending += deltaY;
|
if (target) {
|
||||||
flushScroll();
|
showDebug(`dispatch wheel deltaY=${deltaY.toFixed(0)}`);
|
||||||
} else if (termRef.current) {
|
const wheel = new WheelEvent("wheel", {
|
||||||
// Normal buffer: let xterm.js scroll its own viewport by
|
deltaY,
|
||||||
// the number of lines corresponding to the swipe distance.
|
deltaX: 0,
|
||||||
const lineHeight =
|
deltaZ: 0,
|
||||||
termRef.current.options.fontSize != null
|
clientX: touch.clientX,
|
||||||
? (termRef.current.options.fontSize as number) * 1.2
|
clientY: touch.clientY,
|
||||||
: 10;
|
screenX: touch.screenX,
|
||||||
const lines = Math.round(deltaY / lineHeight);
|
screenY: touch.screenY,
|
||||||
if (lines !== 0) {
|
bubbles: true,
|
||||||
termRef.current.scrollLines(lines);
|
cancelable: true,
|
||||||
}
|
});
|
||||||
|
target.dispatchEvent(wheel);
|
||||||
}
|
}
|
||||||
startY = touch.clientY;
|
startY = touch.clientY;
|
||||||
};
|
};
|
||||||
const onTouchEnd = () => {
|
const onTouchEnd = () => {
|
||||||
if (isScrolling) {
|
|
||||||
flushScroll(true);
|
|
||||||
}
|
|
||||||
isScrolling = false;
|
isScrolling = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features/tool-workshop
|
dir: apps/web/src/components/features/tool-workshop
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides a complete UI for managing custom tool types (compose, dockerfile, manifest) in a workshop interface with list, edit, and mobile-responsive views.
|
Provides a complete UI for managing custom tool types in a "Tool Workshop" interface with list, edit, and mobile-responsive views.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/components/features/.pi-map.index.md
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
map: apps/web/src/components/features/.pi-map.md
|
map: apps/web/src/components/features/.pi-map.md
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ dir: apps/web/src/components/features/tool-workshop
|
|||||||
index: apps/web/src/components/features/tool-workshop/.pi-map.index.md
|
index: apps/web/src/components/features/tool-workshop/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides a complete UI for managing custom tool types (compose, dockerfile, manifest) in a workshop interface with list, edit, and mobile-responsive views.
|
Provides a complete UI for managing custom tool types in a "Tool Workshop" interface with list, edit, and mobile-responsive views.
|
||||||
## files
|
## files
|
||||||
- ToolTypeEditorPanel.tsx | Renders a form panel for creating or editing tool types with support for compose, dockerfile, and manifest definition types | exp: ToolTypeFormState, ToolTypeEditorPanel | dep: ../../icon, ../tool/manifest-editor, ../../../api/tool-types, ../../../api/tool-definitions, React, Icon, ManifestEditor
|
- ToolTypeEditorPanel.tsx | Renders a form panel for creating or editing tool types with support for compose, dockerfile, and manifest definition types | exp: ToolTypeFormState, ToolTypeEditorPanel | dep: ../../icon, ../tool/manifest-editor, ../../../api/tool-types, ../../../api/tool-definitions, React, Icon, ManifestEditor
|
||||||
- ToolTypeListSidebar.tsx | Renders a sidebar component for listing, selecting, creating, and deleting tool types in a "Tool Workshop" interface. | exp: ToolTypeListSidebar | dep: ../../icon, ../../../api/tool-types, React, Icon component, ToolType type
|
- ToolTypeListSidebar.tsx | Renders a sidebar component for listing, selecting, creating, and deleting tool types in a "Tool Workshop" interface. | exp: ToolTypeListSidebar | dep: ../../icon, ../../../api/tool-types, React, Icon component, ToolType type
|
||||||
- ToolWorkshopMobileView.tsx | Renders a mobile-responsive workshop interface for managing tool types with list, detail, and edit views. | exp: MobileView, ToolWorkshopMobileView | dep: react, ../mobile/mobile-list-view, ../mobile/mobile-detail-view, ../mobile/mobile-edit-view, ../mobile/mobile-fab, ../tool/manifest-editor, ../../icon, ../../../api/tool-types, ./ToolTypeEditorPanel, ../../../api/tool-definitions, mobile-list-view, mobile-detail-view, mobile-edit-view, mobile-fab, manifest-editor, icon, tool-types, tool-definitions
|
- ToolWorkshopMobileView.tsx | Renders a mobile-responsive view for managing tool types with list, detail, and edit modes | exp: MobileView, ToolWorkshopMobileView | dep: ../mobile/mobile-list-view, ../mobile/mobile-detail-view, ../mobile/mobile-edit-view, ../mobile/mobile-fab, ../../../api/tool-types, ./ToolTypeEditorPanel, MobileListView, MobileDetailView, MobileEditView, MobileFAB, ToolType, ToolTypeFormState
|
||||||
## arch
|
## arch
|
||||||
Feature-based component composition with split-pane layout (sidebar + editor panel), responsive mobile adaptation, and CRUD operations for tool type definitions.
|
Uses a split-pane sidebar/detail panel pattern with dedicated mobile breakpoint handling, separating list navigation from form editing concerns across three specialized view components.
|
||||||
## tags
|
## tags
|
||||||
tool, mobile, view, type, types, editor, list, workshop
|
tool, mobile, type, view, types, list, panel, editor
|
||||||
## symbols
|
## symbols
|
||||||
- ToolTypeFormState
|
- ToolTypeFormState
|
||||||
- ToolTypeEditorPanel
|
- ToolTypeEditorPanel
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { MobileListView } from "../mobile/mobile-list-view";
|
import { MobileListView } from "../mobile/mobile-list-view";
|
||||||
import { MobileDetailView } from "../mobile/mobile-detail-view";
|
import { MobileDetailView } from "../mobile/mobile-detail-view";
|
||||||
import { MobileEditView } from "../mobile/mobile-edit-view";
|
import { MobileEditView } from "../mobile/mobile-edit-view";
|
||||||
import { MobileFAB } from "../mobile/mobile-fab";
|
import { MobileFAB } from "../mobile/mobile-fab";
|
||||||
import { ManifestEditor } from "../tool/manifest-editor";
|
|
||||||
import { Icon } from "../../icon";
|
|
||||||
import type { ToolType } from "../../../api/tool-types";
|
import type { ToolType } from "../../../api/tool-types";
|
||||||
import type { ToolTypeFormState } from "./ToolTypeEditorPanel";
|
import type { ToolTypeFormState } from "./ToolTypeEditorPanel";
|
||||||
import type { ToolDefinitionManifest } from "../../../api/tool-definitions";
|
|
||||||
|
|
||||||
export type MobileView = "list" | "detail" | "edit";
|
export type MobileView = "list" | "detail" | "edit";
|
||||||
|
|
||||||
@@ -18,18 +13,13 @@ interface ToolWorkshopMobileViewProps {
|
|||||||
mobileView: MobileView;
|
mobileView: MobileView;
|
||||||
isCreating: boolean;
|
isCreating: boolean;
|
||||||
toolTypeForm: ToolTypeFormState;
|
toolTypeForm: ToolTypeFormState;
|
||||||
manifestData: Record<string, unknown> | null;
|
|
||||||
manifestDefinitionId: string | null;
|
|
||||||
baseDefinitions: ToolDefinitionManifest[];
|
|
||||||
toolTypeError: string | null;
|
toolTypeError: string | null;
|
||||||
toolTypeDirty: boolean;
|
|
||||||
onViewChange: (view: MobileView) => void;
|
onViewChange: (view: MobileView) => void;
|
||||||
onSelect: (toolType: ToolType) => void;
|
onSelect: (toolType: ToolType) => void;
|
||||||
onCreate: () => void;
|
onCreate: () => void;
|
||||||
onDelete: (id: string) => void;
|
onDelete: (id: string) => void;
|
||||||
onFormChange: (changes: Partial<ToolTypeFormState>) => void;
|
onFormChange: (changes: Partial<ToolTypeFormState>) => void;
|
||||||
onManifestChange: (manifest: Record<string, unknown> | null) => void;
|
onSubmit: () => void;
|
||||||
onSubmit: (e?: React.FormEvent) => Promise<boolean>;
|
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,41 +29,15 @@ export const ToolWorkshopMobileView = ({
|
|||||||
mobileView,
|
mobileView,
|
||||||
isCreating,
|
isCreating,
|
||||||
toolTypeForm,
|
toolTypeForm,
|
||||||
manifestData,
|
|
||||||
manifestDefinitionId,
|
|
||||||
baseDefinitions,
|
|
||||||
toolTypeError,
|
toolTypeError,
|
||||||
toolTypeDirty,
|
|
||||||
onViewChange,
|
onViewChange,
|
||||||
onSelect,
|
onSelect,
|
||||||
onCreate,
|
onCreate,
|
||||||
onDelete,
|
onDelete,
|
||||||
onFormChange,
|
onFormChange,
|
||||||
onManifestChange,
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: ToolWorkshopMobileViewProps) => {
|
}: ToolWorkshopMobileViewProps) => {
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
setIsSaving(true);
|
|
||||||
try {
|
|
||||||
const ok = await onSubmit();
|
|
||||||
if (ok) {
|
|
||||||
onViewChange(isCreating ? "list" : "edit");
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setIsSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
|
||||||
if (selectedToolType) {
|
|
||||||
void onDelete(selectedToolType.id);
|
|
||||||
onViewChange("list");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (mobileView === "list") {
|
if (mobileView === "list") {
|
||||||
return (
|
return (
|
||||||
<div className="mobile-page">
|
<div className="mobile-page">
|
||||||
@@ -85,16 +49,15 @@ export const ToolWorkshopMobileView = ({
|
|||||||
items={toolTypes.map((t) => ({
|
items={toolTypes.map((t) => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
title: t.display_name,
|
title: t.display_name,
|
||||||
subtitle: `${t.category || "Uncategorized"} · ${t.definition_type} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
|
subtitle: `${t.category || "Uncategorized"} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
|
||||||
}))}
|
}))}
|
||||||
onItemClick={(id) => {
|
onItemClick={(id) => {
|
||||||
const toolType = toolTypes.find((t) => t.id === id);
|
const toolType = toolTypes.find((t) => t.id === id);
|
||||||
if (toolType) {
|
if (toolType) {
|
||||||
onSelect(toolType);
|
onSelect(toolType);
|
||||||
onViewChange("edit");
|
onViewChange("detail");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onItemDelete={(id) => onDelete(id)}
|
|
||||||
emptyMessage="No tool types yet"
|
emptyMessage="No tool types yet"
|
||||||
/>
|
/>
|
||||||
<MobileFAB
|
<MobileFAB
|
||||||
@@ -108,70 +71,64 @@ export const ToolWorkshopMobileView = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mobileView === "detail" && selectedToolType) {
|
if (mobileView === "detail" && selectedToolType) {
|
||||||
const fields = [
|
|
||||||
{ label: "Name", value: selectedToolType.name },
|
|
||||||
{ label: "Display Name", value: selectedToolType.display_name },
|
|
||||||
{ label: "Description", value: selectedToolType.description },
|
|
||||||
{ label: "Category", value: selectedToolType.category },
|
|
||||||
{ label: "Interface Type", value: selectedToolType.interface_type },
|
|
||||||
{
|
|
||||||
label: "Requires Port",
|
|
||||||
value: selectedToolType.requires_port,
|
|
||||||
type: "boolean" as const,
|
|
||||||
},
|
|
||||||
...(selectedToolType.requires_port
|
|
||||||
? [{ label: "Default Port", value: selectedToolType.default_port }]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
label: "Definition Type",
|
|
||||||
value: selectedToolType.definition_type,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Startup Command",
|
|
||||||
value: selectedToolType.startup_command,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Readiness Command",
|
|
||||||
value: selectedToolType.readiness_probe?.command ?? null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Readiness Timeout",
|
|
||||||
value: selectedToolType.readiness_probe?.timeout ?? null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Readiness Interval",
|
|
||||||
value: selectedToolType.readiness_probe?.interval ?? null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Required Variables",
|
|
||||||
value: selectedToolType.required_variables?.join(", ") ?? null,
|
|
||||||
},
|
|
||||||
...(selectedToolType.definition_type !== "manifest"
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label:
|
|
||||||
selectedToolType.definition_type === "compose"
|
|
||||||
? "Compose Template"
|
|
||||||
: "Dockerfile Template",
|
|
||||||
value:
|
|
||||||
selectedToolType.definition_type === "compose"
|
|
||||||
? selectedToolType.compose_template
|
|
||||||
: selectedToolType.dockerfile_template,
|
|
||||||
type: "code" as const,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobileDetailView
|
<MobileDetailView
|
||||||
title={selectedToolType.display_name}
|
title={selectedToolType.display_name}
|
||||||
subtitle={`${selectedToolType.name} · ${selectedToolType.definition_type}`}
|
subtitle={`${selectedToolType.name} · ${selectedToolType.definition_type} · ${selectedToolType.interface_type === "web" ? `Port ${selectedToolType.default_port}` : "Terminal"}`}
|
||||||
fields={fields}
|
fields={[
|
||||||
|
{ label: "Name", value: selectedToolType.name },
|
||||||
|
{ label: "Display Name", value: selectedToolType.display_name },
|
||||||
|
{ label: "Description", value: selectedToolType.description },
|
||||||
|
{ label: "Category", value: selectedToolType.category },
|
||||||
|
{ label: "Interface Type", value: selectedToolType.interface_type },
|
||||||
|
{
|
||||||
|
label: "Requires Port",
|
||||||
|
value: selectedToolType.requires_port,
|
||||||
|
type: "boolean",
|
||||||
|
},
|
||||||
|
{ label: "Default Port", value: selectedToolType.default_port },
|
||||||
|
{
|
||||||
|
label: "Definition Type",
|
||||||
|
value: selectedToolType.definition_type,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Startup Command",
|
||||||
|
value: selectedToolType.startup_command,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Readiness Command",
|
||||||
|
value: selectedToolType.readiness_probe?.command ?? null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Readiness Timeout",
|
||||||
|
value: selectedToolType.readiness_probe?.timeout ?? null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Readiness Interval",
|
||||||
|
value: selectedToolType.readiness_probe?.interval ?? null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Required Variables",
|
||||||
|
value: selectedToolType.required_variables?.join(", ") ?? null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Compose Template",
|
||||||
|
value: selectedToolType.compose_template,
|
||||||
|
type: "code",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Dockerfile Template",
|
||||||
|
value: selectedToolType.dockerfile_template,
|
||||||
|
type: "code",
|
||||||
|
},
|
||||||
|
]}
|
||||||
onEdit={() => {
|
onEdit={() => {
|
||||||
onViewChange("edit");
|
onViewChange("edit");
|
||||||
}}
|
}}
|
||||||
onDelete={handleDelete}
|
onDelete={() => {
|
||||||
|
void onDelete(selectedToolType.id);
|
||||||
|
onViewChange("list");
|
||||||
|
}}
|
||||||
onBack={() => {
|
onBack={() => {
|
||||||
onViewChange("list");
|
onViewChange("list");
|
||||||
}}
|
}}
|
||||||
@@ -180,41 +137,18 @@ export const ToolWorkshopMobileView = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mobileView === "edit") {
|
if (mobileView === "edit") {
|
||||||
const templateValue =
|
|
||||||
toolTypeForm.definition_type === "compose"
|
|
||||||
? toolTypeForm.compose_template
|
|
||||||
: toolTypeForm.dockerfile_template;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobileEditView
|
<MobileEditView
|
||||||
title={isCreating ? "Create Tool Type" : "Edit Tool Type"}
|
title={isCreating ? "Create Tool Type" : "Edit Tool Type"}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
onSave={handleSave}
|
onSave={() => {
|
||||||
onDelete={isCreating ? undefined : handleDelete}
|
onSubmit();
|
||||||
deleteLabel="Delete Tool Type"
|
if (!toolTypeError) {
|
||||||
isSaving={isSaving}
|
onViewChange("list");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isSaving={false}
|
||||||
>
|
>
|
||||||
<div className="mobile-form-group">
|
|
||||||
<label className="mobile-form-label">Definition Type</label>
|
|
||||||
<select
|
|
||||||
value={toolTypeForm.definition_type}
|
|
||||||
onChange={(e) =>
|
|
||||||
onFormChange({
|
|
||||||
definition_type: e.target.value as
|
|
||||||
| "compose"
|
|
||||||
| "dockerfile"
|
|
||||||
| "manifest",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="mobile-form-select"
|
|
||||||
disabled={!isCreating}
|
|
||||||
>
|
|
||||||
<option value="compose">Docker Compose</option>
|
|
||||||
<option value="dockerfile">Dockerfile</option>
|
|
||||||
<option value="manifest">Manifest (Declarative)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
<div className="mobile-form-group">
|
||||||
<label className="mobile-form-label">Name *</label>
|
<label className="mobile-form-label">Name *</label>
|
||||||
<input
|
<input
|
||||||
@@ -223,11 +157,8 @@ export const ToolWorkshopMobileView = ({
|
|||||||
onChange={(e) => onFormChange({ name: e.target.value })}
|
onChange={(e) => onFormChange({ name: e.target.value })}
|
||||||
className="mobile-form-input"
|
className="mobile-form-input"
|
||||||
placeholder="e.g., my-tool"
|
placeholder="e.g., my-tool"
|
||||||
disabled={!isCreating}
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
<div className="mobile-form-group">
|
||||||
<label className="mobile-form-label">Display Name *</label>
|
<label className="mobile-form-label">Display Name *</label>
|
||||||
<input
|
<input
|
||||||
@@ -236,10 +167,8 @@ export const ToolWorkshopMobileView = ({
|
|||||||
onChange={(e) => onFormChange({ display_name: e.target.value })}
|
onChange={(e) => onFormChange({ display_name: e.target.value })}
|
||||||
className="mobile-form-input"
|
className="mobile-form-input"
|
||||||
placeholder="e.g., My Tool"
|
placeholder="e.g., My Tool"
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
<div className="mobile-form-group">
|
||||||
<label className="mobile-form-label">Description</label>
|
<label className="mobile-form-label">Description</label>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -250,7 +179,6 @@ export const ToolWorkshopMobileView = ({
|
|||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
<div className="mobile-form-group">
|
||||||
<label className="mobile-form-label">Category</label>
|
<label className="mobile-form-label">Category</label>
|
||||||
<input
|
<input
|
||||||
@@ -261,179 +189,130 @@ export const ToolWorkshopMobileView = ({
|
|||||||
placeholder="e.g., development"
|
placeholder="e.g., development"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
<div className="mobile-form-group">
|
||||||
<label className="mobile-form-label">Interface Type</label>
|
<label className="mobile-form-label">Interface Type</label>
|
||||||
<select
|
<select
|
||||||
value={toolTypeForm.interface_type}
|
value={toolTypeForm.interface_type}
|
||||||
onChange={(e) => {
|
onChange={(e) =>
|
||||||
const value = e.target.value as "web" | "terminal";
|
|
||||||
onFormChange({
|
onFormChange({
|
||||||
interface_type: value,
|
interface_type: e.target.value as "web" | "terminal",
|
||||||
requires_port: value === "web",
|
})
|
||||||
default_port: value === "web" ? toolTypeForm.default_port : "",
|
}
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="mobile-form-select"
|
className="mobile-form-select"
|
||||||
>
|
>
|
||||||
<option value="web">Web</option>
|
<option value="web">Web</option>
|
||||||
<option value="terminal">Terminal</option>
|
<option value="terminal">Terminal</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mobile-form-group">
|
||||||
<div className="mobile-form-group mobile-form-row">
|
<label className="mobile-form-label">Requires Port</label>
|
||||||
<label className="mobile-form-label mobile-form-checkbox-label">
|
<input
|
||||||
<input
|
type="checkbox"
|
||||||
type="checkbox"
|
checked={toolTypeForm.requires_port}
|
||||||
checked={toolTypeForm.requires_port}
|
onChange={(e) => onFormChange({ requires_port: e.target.checked })}
|
||||||
onChange={(e) =>
|
className="mobile-form-checkbox"
|
||||||
onFormChange({ requires_port: e.target.checked })
|
/>
|
||||||
}
|
</div>
|
||||||
className="mobile-form-checkbox"
|
<div className="mobile-form-group">
|
||||||
/>
|
<label className="mobile-form-label">Default Port</label>
|
||||||
Requires Port
|
<input
|
||||||
</label>
|
type="text"
|
||||||
|
value={toolTypeForm.default_port}
|
||||||
|
onChange={(e) => onFormChange({ default_port: e.target.value })}
|
||||||
|
className="mobile-form-input"
|
||||||
|
placeholder="e.g., 8080"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Definition Type</label>
|
||||||
|
<select
|
||||||
|
value={toolTypeForm.definition_type}
|
||||||
|
onChange={(e) =>
|
||||||
|
onFormChange({
|
||||||
|
definition_type: e.target.value as "compose" | "dockerfile",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="mobile-form-select"
|
||||||
|
>
|
||||||
|
<option value="compose">Compose</option>
|
||||||
|
<option value="dockerfile">Dockerfile</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Startup Command</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={toolTypeForm.startup_command}
|
||||||
|
onChange={(e) => onFormChange({ startup_command: e.target.value })}
|
||||||
|
className="mobile-form-input"
|
||||||
|
placeholder="Command to run on startup"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{toolTypeForm.requires_port && (
|
|
||||||
<div className="mobile-form-group">
|
|
||||||
<label className="mobile-form-label">Default Port *</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={toolTypeForm.default_port}
|
|
||||||
onChange={(e) =>
|
|
||||||
onFormChange({ default_port: e.target.value })
|
|
||||||
}
|
|
||||||
className="mobile-form-input"
|
|
||||||
placeholder="e.g., 8080"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{toolTypeForm.interface_type === "terminal" && (
|
|
||||||
<div className="mobile-form-group">
|
|
||||||
<label className="mobile-form-label">Startup Command</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={toolTypeForm.startup_command}
|
|
||||||
onChange={(e) =>
|
|
||||||
onFormChange({ startup_command: e.target.value })
|
|
||||||
}
|
|
||||||
className="mobile-form-input"
|
|
||||||
placeholder="Command to run on startup"
|
|
||||||
/>
|
|
||||||
<small className="mobile-form-help">
|
|
||||||
Command to run before the interactive shell for each new
|
|
||||||
terminal session.
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{toolTypeForm.definition_type === "manifest" ? (
|
|
||||||
<div className="mobile-manifest-editor-wrapper">
|
|
||||||
<ManifestEditor
|
|
||||||
manifest={manifestData}
|
|
||||||
baseDefinitions={baseDefinitions}
|
|
||||||
onChange={(m) => onManifestChange(m)}
|
|
||||||
definitionId={manifestDefinitionId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="mobile-form-group">
|
|
||||||
<label className="mobile-form-label">
|
|
||||||
{toolTypeForm.definition_type === "compose"
|
|
||||||
? "Compose Template *"
|
|
||||||
: "Dockerfile Template *"}
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={templateValue}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (toolTypeForm.definition_type === "compose") {
|
|
||||||
onFormChange({ compose_template: e.target.value });
|
|
||||||
} else {
|
|
||||||
onFormChange({ dockerfile_template: e.target.value });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="mobile-form-textarea mobile-form-code"
|
|
||||||
placeholder={
|
|
||||||
toolTypeForm.definition_type === "compose"
|
|
||||||
? "version: '3'"
|
|
||||||
: "FROM ubuntu:22.04"
|
|
||||||
}
|
|
||||||
rows={10}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
<div className="mobile-form-group">
|
||||||
<label className="mobile-form-label">Readiness Command</label>
|
<label className="mobile-form-label">Readiness Command</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={toolTypeForm.readiness_command}
|
value={toolTypeForm.readiness_command}
|
||||||
onChange={(e) =>
|
onChange={(e) => onFormChange({ readiness_command: e.target.value })}
|
||||||
onFormChange({ readiness_command: e.target.value })
|
|
||||||
}
|
|
||||||
className="mobile-form-input"
|
className="mobile-form-input"
|
||||||
placeholder="e.g., curl -f http://localhost:8080/health"
|
placeholder="e.g., curl -f http://localhost:8080/health"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
<div className="mobile-form-group">
|
||||||
<label className="mobile-form-label">Readiness Timeout (seconds)</label>
|
<label className="mobile-form-label">Readiness Timeout</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="text"
|
||||||
value={toolTypeForm.readiness_timeout}
|
value={toolTypeForm.readiness_timeout}
|
||||||
onChange={(e) =>
|
onChange={(e) => onFormChange({ readiness_timeout: e.target.value })}
|
||||||
onFormChange({ readiness_timeout: e.target.value })
|
|
||||||
}
|
|
||||||
className="mobile-form-input"
|
className="mobile-form-input"
|
||||||
placeholder="30"
|
placeholder="30"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
<div className="mobile-form-group">
|
||||||
<label className="mobile-form-label">Readiness Interval (seconds)</label>
|
<label className="mobile-form-label">Readiness Interval</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="text"
|
||||||
value={toolTypeForm.readiness_interval}
|
value={toolTypeForm.readiness_interval}
|
||||||
onChange={(e) =>
|
onChange={(e) => onFormChange({ readiness_interval: e.target.value })}
|
||||||
onFormChange({ readiness_interval: e.target.value })
|
|
||||||
}
|
|
||||||
className="mobile-form-input"
|
className="mobile-form-input"
|
||||||
placeholder="2"
|
placeholder="2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mobile-form-group">
|
<div className="mobile-form-group">
|
||||||
<label className="mobile-form-label">Required Variables</label>
|
<label className="mobile-form-label">Required Variables</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={toolTypeForm.required_variables}
|
value={toolTypeForm.required_variables}
|
||||||
onChange={(e) =>
|
onChange={(e) => onFormChange({ required_variables: e.target.value })}
|
||||||
onFormChange({ required_variables: e.target.value })
|
|
||||||
}
|
|
||||||
className="mobile-form-input"
|
className="mobile-form-input"
|
||||||
placeholder="VAR1, VAR2, VAR3"
|
placeholder="VAR1, VAR2, VAR3"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{toolTypeForm.definition_type === "compose" && (
|
||||||
{toolTypeError && (
|
<div className="mobile-form-group">
|
||||||
<p className="mobile-form-error">
|
<label className="mobile-form-label">Compose Template</label>
|
||||||
<Icon name="warning" size="sm" /> {toolTypeError}
|
<textarea
|
||||||
</p>
|
value={toolTypeForm.compose_template}
|
||||||
|
onChange={(e) => onFormChange({ compose_template: e.target.value })}
|
||||||
|
className="mobile-form-textarea mobile-form-code"
|
||||||
|
placeholder="version: '3'"
|
||||||
|
rows={10}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{toolTypeForm.definition_type === "dockerfile" && (
|
||||||
{toolTypeDirty && (
|
<div className="mobile-form-group">
|
||||||
<button
|
<label className="mobile-form-label">Dockerfile Template</label>
|
||||||
type="button"
|
<textarea
|
||||||
className="secondary-button mobile-discard-button"
|
value={toolTypeForm.dockerfile_template}
|
||||||
onClick={onCancel}
|
onChange={(e) =>
|
||||||
disabled={isSaving}
|
onFormChange({ dockerfile_template: e.target.value })
|
||||||
>
|
}
|
||||||
Discard Changes
|
className="mobile-form-textarea mobile-form-code"
|
||||||
</button>
|
placeholder="FROM ubuntu:22.04"
|
||||||
|
rows={10}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</MobileEditView>
|
</MobileEditView>
|
||||||
);
|
);
|
||||||
@@ -449,16 +328,15 @@ export const ToolWorkshopMobileView = ({
|
|||||||
items={toolTypes.map((t) => ({
|
items={toolTypes.map((t) => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
title: t.display_name,
|
title: t.display_name,
|
||||||
subtitle: `${t.category || "Uncategorized"} · ${t.definition_type} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
|
subtitle: `${t.category || "Uncategorized"} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
|
||||||
}))}
|
}))}
|
||||||
onItemClick={(id) => {
|
onItemClick={(id) => {
|
||||||
const toolType = toolTypes.find((t) => t.id === id);
|
const toolType = toolTypes.find((t) => t.id === id);
|
||||||
if (toolType) {
|
if (toolType) {
|
||||||
onSelect(toolType);
|
onSelect(toolType);
|
||||||
onViewChange("edit");
|
onViewChange("detail");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onItemDelete={(id) => onDelete(id)}
|
|
||||||
emptyMessage="No tool types yet"
|
emptyMessage="No tool types yet"
|
||||||
/>
|
/>
|
||||||
<MobileFAB
|
<MobileFAB
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/hooks
|
dir: apps/web/src/hooks
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides reusable React custom hooks that encapsulate stateful logic, side effects, and API interactions for the web application's UI components, covering data fetching, terminal management, workspace operations, notifications, and mobile-specific behaviors.
|
A collection of custom React hooks providing reusable state management, API integration, and UI behavior logic for the web application.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
map: apps/web/src/.pi-map.md
|
map: apps/web/src/.pi-map.md
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ dir: apps/web/src/hooks
|
|||||||
index: apps/web/src/hooks/.pi-map.index.md
|
index: apps/web/src/hooks/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides reusable React custom hooks that encapsulate stateful logic, side effects, and API interactions for the web application's UI components, covering data fetching, terminal management, workspace operations, notifications, and mobile-specific behaviors.
|
A collection of custom React hooks providing reusable state management, API integration, and UI behavior logic for the web application.
|
||||||
## files
|
## files
|
||||||
- use-async-data.ts | A custom React hook that manages asynchronous data fetching with loading, error, and ready states, plus a manual reload capability. | exp: func:useAsyncData(fetcher: () => Promise<T>, deps: React.DependencyList) → UseAsyncDataResult<T>, call:useState, call:useCallback, call:setStatus, call:setError, call:fetcher, call:setData, call:load, call:useEffect | dep: react
|
- use-async-data.ts | A custom React hook that manages asynchronous data fetching with loading, error, and ready states, plus a manual reload capability. | exp: func:useAsyncData(fetcher: () => Promise<T>, deps: React.DependencyList) → UseAsyncDataResult<T>, call:useState, call:useCallback, call:setStatus, call:setError, call:fetcher, call:setData, call:load, call:useEffect | dep: react
|
||||||
- use-auto-hide.ts | A React custom hook that automatically hides an element after a specified timeout and provides manual controls for showing, hiding, and toggling visibility. | exp: func:useAutoHide(options: AutoHideOptions), call:useState, call:useRef, call:Date.now, call:useCallback, call:setIsVisible, call:clearTimeout, call:setTimeout, call:hide, call:show, call:useEffect | dep: react
|
- use-auto-hide.ts | A React custom hook that automatically hides an element after a specified timeout and provides manual controls for showing, hiding, and toggling visibility. | exp: func:useAutoHide(options: AutoHideOptions), call:useState, call:useRef, call:Date.now, call:useCallback, call:setIsVisible, call:clearTimeout, call:setTimeout, call:hide, call:show, call:useEffect | dep: react
|
||||||
- use-config-profiles.ts | A React custom hook that manages CRUD operations, form state, drag-and-drop ordering, and cyclic dependency prevention for configuration profiles. | exp: useConfigProfiles | dep: react, ../utils/errors, ../api/config-profiles, ../api/projects, ../api/tool-types, ../types
|
- use-config-profiles.ts | A React custom hook that manages config profile CRUD operations, form state, profile inclusion with cycle detection, and drag-and-drop reordering. | exp: useConfigProfiles | dep: react, ../utils/errors, ../api/config-profiles, ../api/projects, ../api/tool-types, ../types
|
||||||
- use-events.test.ts | Tests a React hook that manages Server-Sent Events (SSE) connections with automatic reconnection, backoff, and error handling | dep: vitest, @testing-library/react, ./use-events, ../api/events, use-events hook
|
- use-events.test.ts | Tests a React custom hook that manages Server-Sent Events connections with automatic reconnection, backoff strategies, and error handling. | dep: vitest, @testing-library/react, ./use-events, ../api/events, use-events hook, ../api/events module
|
||||||
- use-events.ts | React hook that manages a Server-Sent Events connection with automatic exponential backoff reconnection, lifecycle event handling, and authentication redirect on 401 errors. | exp: UseEventsReturn, func:useEvents() → UseEventsReturn, call:useState, call:useRef, call:useCallback, call:clearTimeout, call:createEventSource, call:setConnected, call:setError, call:setReconnectCount, call:JSON.parse, call:setEvents, call:es.addEventListener, call:es.close, call:Math.min, call:Math.pow, call:Math.random, call:setTimeout, call:probeEventStreamStatus, call:window.location.assign, call:connect, call:useEffect, call:esRef.current.close | dep: react, ../api/events, ../types/events
|
- use-events.ts | React hook that manages a Server-Sent Events connection with exponential backoff reconnection, event buffering, and auth/rate-limit handling. | exp: UseEventsReturn, func:useEvents() → UseEventsReturn, call:useState, call:useRef, call:useCallback, call:clearTimeout, call:createEventSource, call:setConnected, call:setError, call:setReconnectCount, call:JSON.parse, call:setEvents, call:es.close, call:Math.min, call:Math.pow, call:Math.random, call:setTimeout, call:probeEventStreamStatus, call:window.location.assign, call:connect, call:useEffect, call:esRef.current.close | dep: react, ../api/events, ../types/events
|
||||||
- use-git-repo.ts | A custom React hook that centralizes all git repository operations (branch management, status tracking, commit history, and git actions) into a reusable interface for components. | exp: GitStatus, UseGitRepoResult, func:useGitRepo(projectId: string | undefined, repoId: string | undefined) → UseGitRepoResult, call:useState, call:useCallback, call:setLoading, call:setError, call:fn, call:extractError, call:withLoading, call:listRepositoryBranches, call:setBranches, call:data.branches.map, call:setDefaultBranch, call:getRepositoryStatus, call:setStatus, call:getRepositoryHistory, call:setHistory, call:getCommitDetail, call:setCommitDetail, call:commitChanges, call:refreshStatus, call:pushRepository, call:pullRepository, call:fetchRepository, call:checkoutBranch, call:refreshBranches, call:createBranch, call:deleteBranch, call:mergeBranches, call:refreshHistory, call:useEffect, raise:err | dep: react, ../api/git-repositories
|
- use-git-repo.ts | A custom React hook that centralizes all git repository operations (branch management, status tracking, commit history, and git actions) into a reusable interface for components. | exp: GitStatus, UseGitRepoResult, func:useGitRepo(projectId: string | undefined, repoId: string | undefined) → UseGitRepoResult, call:useState, call:useCallback, call:setLoading, call:setError, call:fn, call:extractError, call:withLoading, call:listRepositoryBranches, call:setBranches, call:data.branches.map, call:setDefaultBranch, call:getRepositoryStatus, call:setStatus, call:getRepositoryHistory, call:setHistory, call:getCommitDetail, call:setCommitDetail, call:commitChanges, call:refreshStatus, call:pushRepository, call:pullRepository, call:fetchRepository, call:checkoutBranch, call:refreshBranches, call:createBranch, call:deleteBranch, call:mergeBranches, call:refreshHistory, call:useEffect, raise:err | dep: react, ../api/git-repositories
|
||||||
- use-instance-actions.ts | A React custom hook that manages instance/session actions including opening, starting, stopping, deleting, force-deleting, recreating tunnels, and renaming with loading states and dirty delete handling. | exp: func:useInstanceActions(options: UseInstanceActionsOptions) → UseInstanceActionsReturn, call:useSessions, call:useState, call:useRef, call:useCallback, call:tabRefs.current.get, call:existing.focus, call:session.tool_type_interfaces?.includes, call:window.open, call:tabRefs.current.set, call:setLoadingSessionId, call:startInstance, call:onRefresh, call:stopInstance, call:deleteInstance, call:setDirtyDeleteSession, call:setDirtyDeleteFiles, call:removeSession, call:recreateInstanceTunnel, call:alert, call:newName.trim, call:renameInstance | dep: react, ../api/sessions, ../state/sessions
|
- use-instance-actions.ts | A custom React hook that provides a centralized interface for managing instance/session actions including opening, starting, stopping, deleting, force-deleting, recreating tunnels, and renaming, with loading states and dirty delete handling for conflict resolution. | exp: func:useInstanceActions(options: UseInstanceActionsOptions) → UseInstanceActionsReturn, call:useSessions, call:useSessionOperations, call:useState, call:useRef, call:useCallback, call:tabRefs.current.get, call:existing.focus, call:session.tool_type_interfaces?.includes, call:window.open, call:tabRefs.current.set, call:setLoadingSessionId, call:startOperation, call:startInstance, call:onRefresh, call:completeOperation, call:stopInstance, call:deleteInstance, call:setDirtyDeleteSession, call:setDirtyDeleteFiles, call:removeSession, call:recreateInstanceTunnel, call:alert, call:newName.trim, call:renameInstance | dep: react, ../api/sessions, ../state/sessions, ../state/session-operations
|
||||||
- use-mobile-viewport.ts | A React hook that tracks whether the viewport width is below a mobile breakpoint (768px) | exp: func:useMobileViewport(), call:useState, call:useEffect, call:setIsMobile, call:window.addEventListener, call:window.removeEventListener | dep: react
|
- use-mobile-viewport.ts | A React hook that tracks whether the viewport width is below a mobile breakpoint (768px) | exp: func:useMobileViewport(), call:useState, call:useEffect, call:setIsMobile, call:window.addEventListener, call:window.removeEventListener | dep: react
|
||||||
- use-notifications.test.tsx | Tests the useNotifications custom React hook with mocked API calls, covering optimistic updates, polling behavior, and error handling. | dep: vitest, @testing-library/react, ./use-notifications, ../state/notifications, ../api/notifications, use-notifications
|
- use-notifications.test.tsx | Tests the useNotifications custom React hook with mocked API calls, covering optimistic updates, polling behavior, and error handling. | dep: vitest, @testing-library/react, ./use-notifications, ../state/notifications, ../api/notifications, use-notifications
|
||||||
- use-notifications.ts | Custom React hook that provides access to notification state and ensures it's used within a NotificationProvider | exp: func:useNotifications(), call:useContext, raise:Error | dep: react, ../state/notifications
|
- use-notifications.ts | Custom React hook that provides access to notification state and ensures it's used within a NotificationProvider | exp: func:useNotifications(), call:useContext, raise:Error | dep: react, ../state/notifications
|
||||||
@@ -23,7 +23,7 @@ Provides reusable React custom hooks that encapsulate stateful logic, side effec
|
|||||||
- use-terminal-page.ts | Manages terminal page state including sessions, keyboard shortcuts, fullscreen mode, mobile viewport handling, and terminal lifecycle operations. | exp: useTerminalPage | dep: react, react-router-dom, ../components/features/terminal/terminal, ../components/features/terminal/terminal-session-tabs, ./use-mobile-viewport, ./use-auto-hide, ./use-virtual-keyboard, ./use-terminal-sessions, ../api/terminal, ./use-special-keys, use-mobile-viewport, use-auto-hide, use-virtual-keyboard, use-terminal-sessions, terminal, terminal-session-tabs, api/terminal, api/sessions, use-special-keys
|
- use-terminal-page.ts | Manages terminal page state including sessions, keyboard shortcuts, fullscreen mode, mobile viewport handling, and terminal lifecycle operations. | exp: useTerminalPage | dep: react, react-router-dom, ../components/features/terminal/terminal, ../components/features/terminal/terminal-session-tabs, ./use-mobile-viewport, ./use-auto-hide, ./use-virtual-keyboard, ./use-terminal-sessions, ../api/terminal, ./use-special-keys, use-mobile-viewport, use-auto-hide, use-virtual-keyboard, use-terminal-sessions, terminal, terminal-session-tabs, api/terminal, api/sessions, use-special-keys
|
||||||
- use-terminal-sessions.ts | React custom hook that manages terminal session state (CRUD operations, active session tracking) for a given instance | exp: UseTerminalSessionsResult, func:useTerminalSessions(instanceId: string) → UseTerminalSessionsResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listTerminalSessions, call:setSessions, call:setActiveSessionId, call:createTerminalSession, call:closeTerminalSession, call:prev.filter, call:renameTerminalSession, call:prev.map, call:resetTerminalSession, call:loadSessions, call:useEffect | dep: react, ../api/terminal
|
- use-terminal-sessions.ts | React custom hook that manages terminal session state (CRUD operations, active session tracking) for a given instance | exp: UseTerminalSessionsResult, func:useTerminalSessions(instanceId: string) → UseTerminalSessionsResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listTerminalSessions, call:setSessions, call:setActiveSessionId, call:createTerminalSession, call:closeTerminalSession, call:prev.filter, call:renameTerminalSession, call:prev.map, call:resetTerminalSession, call:loadSessions, call:useEffect | dep: react, ../api/terminal
|
||||||
- use-theme.ts | React hook that fetches user theme preference on mount and applies it to the document root element via data-theme attribute | exp: func:useTheme(), call:useEffect, call:getUserConfig, call:document.documentElement.removeAttribute, call:document.documentElement.setAttribute | dep: react, ../api/settings
|
- use-theme.ts | React hook that fetches user theme preference on mount and applies it to the document root element via data-theme attribute | exp: func:useTheme(), call:useEffect, call:getUserConfig, call:document.documentElement.removeAttribute, call:document.documentElement.setAttribute | dep: react, ../api/settings
|
||||||
- use-tool-workshop.ts | Custom React hook that manages state and operations for a tool workshop UI, including CRUD operations for tool types and tool definitions with form handling and validation. | exp: useToolWorkshop | dep: react, ../utils/errors, ../api/tool-types, ../api/tool-definitions, ../components/features/tool-workshop/ToolTypeEditorPanel
|
- use-tool-workshop.ts | React custom hook that manages state and operations for a tool workshop UI, including CRUD operations for tool types and tool definitions with form handling and validation. | exp: useToolWorkshop | dep: react, ../utils/errors, ../api/tool-types, ../api/tool-definitions, ../components/features/tool-workshop/ToolTypeEditorPanel
|
||||||
- use-virtual-keyboard.ts | React hook that detects virtual keyboard open/close state and measures its height on mobile devices | exp: func:useVirtualKeyboard(), call:useState, call:useCallback, call:setState, call:useEffect, call:visualViewport.addEventListener, call:window.addEventListener, call:updateKeyboardState, call:visualViewport.removeEventListener, call:window.removeEventListener | dep: react
|
- use-virtual-keyboard.ts | React hook that detects virtual keyboard open/close state and measures its height on mobile devices | exp: func:useVirtualKeyboard(), call:useState, call:useCallback, call:setState, call:useEffect, call:visualViewport.addEventListener, call:window.addEventListener, call:updateKeyboardState, call:visualViewport.removeEventListener, call:window.removeEventListener | dep: react
|
||||||
- use-workspace-actions.ts | Provides a React hook that encapsulates workspace CRUD operations with loading state management and user confirmation dialogs for destructive actions. | exp: UseWorkspaceActionsResult, func:useWorkspaceActions() → UseWorkspaceActionsResult, call:useState, call:useCallback, call:createWorkspace, call:setLoadingId, call:deleteWorkspace, call:onRefresh, call:window.confirm, call:instances.map((i) => `- ${i.name}`).join, call:syncWorkspace, call:updateWorkspace, raise:err | dep: react, ../api/workspaces, ../types/workspace
|
- use-workspace-actions.ts | Provides a React hook that encapsulates workspace CRUD operations with loading state management and user confirmation dialogs for destructive actions. | exp: UseWorkspaceActionsResult, func:useWorkspaceActions() → UseWorkspaceActionsResult, call:useState, call:useCallback, call:createWorkspace, call:setLoadingId, call:deleteWorkspace, call:onRefresh, call:window.confirm, call:instances.map((i) => `- ${i.name}`).join, call:syncWorkspace, call:updateWorkspace, raise:err | dep: react, ../api/workspaces, ../types/workspace
|
||||||
- use-workspace-files.ts | Custom React hook for managing workspace file operations including listing, loading, saving, and navigating files. | exp: UseWorkspaceFilesResult, func:useWorkspaceFiles(workspaceId: string) → UseWorkspaceFilesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaceFiles, call:setEntries, call:setCurrentPath, call:setContent, call:getWorkspaceFileContent, call:saveWorkspaceFile, call:refresh, call:useEffect | dep: react, ../api/workspace-files
|
- use-workspace-files.ts | Custom React hook for managing workspace file operations including listing, loading, saving, and navigating files. | exp: UseWorkspaceFilesResult, func:useWorkspaceFiles(workspaceId: string) → UseWorkspaceFilesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaceFiles, call:setEntries, call:setCurrentPath, call:setContent, call:getWorkspaceFileContent, call:saveWorkspaceFile, call:refresh, call:useEffect | dep: react, ../api/workspace-files
|
||||||
@@ -31,7 +31,7 @@ Provides reusable React custom hooks that encapsulate stateful logic, side effec
|
|||||||
- use-workspace-instances.ts | Custom React hook for managing workspace instances with CRUD operations, loading states, and error handling. | exp: UseWorkspaceInstancesResult, func:useWorkspaceInstances(workspaceId: string) → UseWorkspaceInstancesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaceInstances, call:setInstances, call:createWorkspaceInstance, call:refresh, call:useEffect | dep: react, ../api/workspace-instances, ../api/sessions
|
- use-workspace-instances.ts | Custom React hook for managing workspace instances with CRUD operations, loading states, and error handling. | exp: UseWorkspaceInstancesResult, func:useWorkspaceInstances(workspaceId: string) → UseWorkspaceInstancesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaceInstances, call:setInstances, call:createWorkspaceInstance, call:refresh, call:useEffect | dep: react, ../api/workspace-instances, ../api/sessions
|
||||||
- use-workspaces.ts | Custom React hook that fetches and manages workspace data with loading and error states. | exp: UseWorkspacesResult, func:useWorkspaces(projectId: string, repoId: string) → UseWorkspacesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaces, call:listAllWorkspaces, call:setWorkspaces, call:useEffect, call:refresh | dep: react, ../api/workspaces, ../types/workspace
|
- use-workspaces.ts | Custom React hook that fetches and manages workspace data with loading and error states. | exp: UseWorkspacesResult, func:useWorkspaces(projectId: string, repoId: string) → UseWorkspacesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaces, call:listAllWorkspaces, call:setWorkspaces, call:useEffect, call:refresh | dep: react, ../api/workspaces, ../types/workspace
|
||||||
## arch
|
## arch
|
||||||
Follows a custom hook composition pattern where each hook encapsulates a single domain concern (data fetching, CRUD operations, UI state, device detection), often combining useState/useEffect/useCallback with SWR-style async data management, and layering higher-level hooks (use-terminal-page) from lower-level ones (use-mobile-viewport, use-virtual-keyboard).
|
Follows a feature-based composition pattern where each hook encapsulates a specific domain concern (data fetching, CRUD operations, terminal/session management, UI interactions), often combining React state with API calls, side effects, and provider context integration.
|
||||||
## tags
|
## tags
|
||||||
call:set, call:use, workspace, react, state, terminal, api, callback
|
call:set, call:use, workspace, react, state, terminal, api, callback
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export const useConfigProfiles = () => {
|
|||||||
setDragOverIndex(null);
|
setDragOverIndex(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e?: React.FormEvent): Promise<boolean> => {
|
const handleSubmit = async (e?: React.FormEvent) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
setError(null);
|
setError(null);
|
||||||
setSaveStatus("saving");
|
setSaveStatus("saving");
|
||||||
@@ -201,7 +201,7 @@ export const useConfigProfiles = () => {
|
|||||||
if (!formData.name?.trim()) {
|
if (!formData.name?.trim()) {
|
||||||
setError("Name is required");
|
setError("Name is required");
|
||||||
setSaveStatus("error");
|
setSaveStatus("error");
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -224,11 +224,9 @@ export const useConfigProfiles = () => {
|
|||||||
const refreshed = (await listConfigProfiles()).find((p) => p.id === selectedProfile.id);
|
const refreshed = (await listConfigProfiles()).find((p) => p.id === selectedProfile.id);
|
||||||
if (refreshed) populateForm(refreshed);
|
if (refreshed) populateForm(refreshed);
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(extractErrorMessage(err));
|
setError(extractErrorMessage(err));
|
||||||
setSaveStatus("error");
|
setSaveStatus("error");
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -151,13 +151,13 @@ export const useToolWorkshop = () => {
|
|||||||
resetToolTypeForm();
|
resetToolTypeForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToolTypeSubmit = async (e?: React.FormEvent): Promise<boolean> => {
|
const handleToolTypeSubmit = async (e?: React.FormEvent) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
setToolTypeError(null);
|
setToolTypeError(null);
|
||||||
|
|
||||||
if (!toolTypeForm.name.trim() || !toolTypeForm.display_name.trim()) {
|
if (!toolTypeForm.name.trim() || !toolTypeForm.display_name.trim()) {
|
||||||
setToolTypeError("Name and display name are required");
|
setToolTypeError("Name and display name are required");
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -166,7 +166,7 @@ export const useToolWorkshop = () => {
|
|||||||
isNaN(Number(toolTypeForm.default_port)))
|
isNaN(Number(toolTypeForm.default_port)))
|
||||||
) {
|
) {
|
||||||
setToolTypeError("Default port is required and must be a number");
|
setToolTypeError("Default port is required and must be a number");
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolTypeForm.definition_type !== "manifest") {
|
if (toolTypeForm.definition_type !== "manifest") {
|
||||||
@@ -179,13 +179,13 @@ export const useToolWorkshop = () => {
|
|||||||
setToolTypeError(
|
setToolTypeError(
|
||||||
`${toolTypeForm.definition_type === "compose" ? "Compose" : "Dockerfile"} template is required`,
|
`${toolTypeForm.definition_type === "compose" ? "Compose" : "Dockerfile"} template is required`,
|
||||||
);
|
);
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
} else if (!manifestData) {
|
} else if (!manifestData) {
|
||||||
setToolTypeError(
|
setToolTypeError(
|
||||||
"Manifest data is required for manifest definition type",
|
"Manifest data is required for manifest definition type",
|
||||||
);
|
);
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const variables = toolTypeForm.required_variables
|
const variables = toolTypeForm.required_variables
|
||||||
@@ -307,10 +307,8 @@ export const useToolWorkshop = () => {
|
|||||||
setToolTypeDirty(false);
|
setToolTypeDirty(false);
|
||||||
}
|
}
|
||||||
await loadData();
|
await loadData();
|
||||||
return true;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setToolTypeError(extractErrorMessage(err));
|
setToolTypeError(extractErrorMessage(err));
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/pages
|
dir: apps/web/src/pages
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains top-level page components that render the main UI views for the web application, each corresponding to a distinct route and feature area.
|
Contains top-level page components that serve as route endpoints for the web application's primary UI surfaces, each handling a specific domain area (dashboard, projects, workspaces, git, settings, etc.) with responsive layouts and CRUD operations.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
map: apps/web/src/.pi-map.md
|
map: apps/web/src/.pi-map.md
|
||||||
|
|||||||
@@ -4,30 +4,30 @@ dir: apps/web/src/pages
|
|||||||
index: apps/web/src/pages/.pi-map.index.md
|
index: apps/web/src/pages/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains top-level page components that render the main UI views for the web application, each corresponding to a distinct route and feature area.
|
Contains top-level page components that serve as route endpoints for the web application's primary UI surfaces, each handling a specific domain area (dashboard, projects, workspaces, git, settings, etc.) with responsive layouts and CRUD operations.
|
||||||
## files
|
## files
|
||||||
- ConfigProfilesPage.tsx | Renders a configuration profiles management page with responsive desktop/mobile layouts for creating, editing, previewing, and organizing config profiles. | exp: ConfigProfilesPage | dep: react, ../components/data-states, ../hooks/use-mobile-viewport, ../hooks/use-config-profiles, ../components/features/config-profiles/ConfigProfileListSidebar, ../components/features/config-profiles/ConfigProfileEditorPanel, ../components/features/config-profiles/ConfigProfilesMobileView, use-mobile-viewport, use-config-profiles, data-states, ConfigProfileListSidebar, ConfigProfileEditorPanel, ConfigProfilesMobileView
|
- ConfigProfilesPage.tsx | Renders a responsive configuration profiles management page with sidebar list and editor panel for desktop, and a dedicated mobile view for creating, editing, and managing config profiles. | exp: ConfigProfilesPage | dep: react, ../components/data-states, ../hooks/use-mobile-viewport, ../hooks/use-config-profiles, ../components/features/config-profiles/ConfigProfileListSidebar, ../components/features/config-profiles/ConfigProfileEditorPanel, ../components/features/config-profiles/ConfigProfilesMobileView
|
||||||
- DashboardPage.test.tsx | Unit tests for the DashboardPage component verifying overview loading and error retry behavior | dep: @testing-library/jest-dom/vitest, @testing-library/react, react-router-dom, vitest, ./DashboardPage, ../state/sessions, DashboardPage, SessionsProvider
|
- DashboardPage.test.tsx | Unit tests for the DashboardPage component verifying overview loading and error retry behavior | dep: @testing-library/jest-dom/vitest, @testing-library/react, react-router-dom, vitest, ./DashboardPage, ../state/sessions, DashboardPage, SessionsProvider
|
||||||
- DashboardPage.tsx | Renders a dashboard homepage that displays workspace overview, active/recent sessions, summary statistics, and polling health checks for running instances. | exp: HomePage | dep: react, react-router-dom, ../api/dashboard, ../api/sessions, ../components/data-states, ../components/features/session/session-list, ../hooks/use-instance-actions, ../state/sessions
|
- DashboardPage.tsx | Renders a dashboard homepage that displays workspace overview, active/recent sessions, summary statistics, and polling health checks for running instances. | exp: HomePage | dep: react, react-router-dom, ../api/dashboard, ../api/sessions, ../components/data-states, ../components/features/session/session-list, ../hooks/use-instance-actions, ../state/sessions
|
||||||
- GitHistoryPage.tsx | Renders a Git commit history page with branch selection, commit list with graph visualization, and a detail panel showing commit metadata, stats, and diffs. | exp: GitHistoryPage | dep: react, react-router-dom, ../api/git-repositories, ../components/data-states, ../components/icon, ../hooks/use-async-data
|
- GitHistoryPage.tsx | Renders a Git commit history page with branch selection, commit list with graph visualization, and a detail panel showing commit metadata, stats, and diffs. | exp: GitHistoryPage | dep: react, react-router-dom, ../api/git-repositories, ../components/data-states, ../components/icon, ../hooks/use-async-data
|
||||||
- GitRepositoriesPage.tsx | Displays and manages a project's Git repositories with CRUD operations including listing, creating, navigating to history, and deleting with confirmation | exp: GitRepositoriesPage | dep: react, react-router-dom, ../api/git-repositories, ../components/data-states, ../components/icon, ../components/features/project/repository-create-dialog, ../hooks/use-async-data
|
- GitRepositoriesPage.tsx | Displays and manages a project's Git repositories with CRUD operations including listing, creating, navigating to history, and deleting with confirmation | exp: GitRepositoriesPage | dep: react, react-router-dom, ../api/git-repositories, ../components/data-states, ../components/icon, ../components/features/project/repository-create-dialog, ../hooks/use-async-data
|
||||||
- PlaceholderPage.tsx | Exports three simple React page components (PlaceholderPage, NotFoundPage, LoginRedirectPage) for a frontend scaffold. | exp: PlaceholderPage, NotFoundPage, LoginRedirectPage | dep: ../components/icon, React
|
- PlaceholderPage.tsx | Exports three simple React page components (PlaceholderPage, NotFoundPage, LoginRedirectPage) for a frontend scaffold. | exp: PlaceholderPage, NotFoundPage, LoginRedirectPage | dep: ../components/icon, React
|
||||||
- ProfilePage.tsx | A React component that renders a responsive profile page with form editing, avatar upload, and save functionality for both mobile and desktop viewports. | exp: ProfilePage | dep: react, ../api/profile, ../components/data-states, ../components/icon, ../components/features/profile/ProfileMobileView, ../state/auth, ../hooks/use-async-data, ../hooks/use-mobile-viewport
|
- ProfilePage.tsx | A React component that displays and allows editing of a user profile, including name, email, and avatar upload with validation. | exp: ProfilePage | dep: react, ../api/profile, ../components/data-states, ../components/icon, ../state/auth, ../hooks/use-async-data
|
||||||
- ProjectSettingsPage.tsx | Renders a project settings page with tabbed navigation for general settings, repositories, and members, including project data fetching, editing, and deletion capabilities. | exp: ProjectSettingsPage | dep: react, react-router-dom, ../components/features/settings/settings-tab-layout, ../components/features/project/repositories-settings-tab, ../api/client, ../types
|
- ProjectSettingsPage.tsx | Renders a project settings page with tabbed navigation for general settings, repositories, and members, including project data fetching, editing, and deletion capabilities. | exp: ProjectSettingsPage | dep: react, react-router-dom, ../components/features/settings/settings-tab-layout, ../components/features/project/repositories-settings-tab, ../api/client, ../types
|
||||||
- ProjectsPage.test.tsx | Unit tests for the ProjectsPage component covering loading, empty, error, create, edit, and delete states with API mocking. | dep: @testing-library/react, react-router-dom, vitest, ./ProjectsPage, ../api/projects, ProjectsPage
|
- ProjectsPage.test.tsx | Unit tests for the ProjectsPage component covering loading, empty, error, create, edit, and delete states with API mocking. | dep: @testing-library/react, react-router-dom, vitest, ./ProjectsPage, ../api/projects, ProjectsPage
|
||||||
- ProjectsPage.tsx | Renders a responsive projects management page with separate mobile and desktop layouts, supporting project CRUD operations, repository management, and workspace actions. | exp: ProjectsPage | dep: react, ../components/data-states, ../components/icon, ../hooks/use-mobile-viewport, ../components/features/project/ProjectCard, ../components/features/project/ProjectDialog, ../components/features/project/repository-create-dialog, ../components/features/mobile/mobile-list-view, ../components/features/mobile/mobile-fab, ../hooks/use-projects, ../types, use-mobile-viewport, use-projects, data-states, icon, ProjectCard, ProjectDialog, RepositoryCreateDialog, MobileListView, MobileFAB
|
- ProjectsPage.tsx | Renders a responsive projects management page with separate mobile and desktop layouts, supporting project CRUD operations, repository management, and workspace actions. | exp: ProjectsPage | dep: react, ../components/data-states, ../components/icon, ../hooks/use-mobile-viewport, ../components/features/project/ProjectCard, ../components/features/project/ProjectDialog, ../components/features/project/repository-create-dialog, ../components/features/mobile/mobile-list-view, ../components/features/mobile/mobile-fab, ../hooks/use-projects, ../types
|
||||||
- SessionsPage.tsx | Renders a sessions management page that displays, polls health for, and handles CRUD operations on development environment sessions with dirty delete confirmation. | exp: SessionsPage | dep: react, ../api/sessions, ../api/settings, ../components/data-states, ../components/features/session/session-list, ../components/features/session/session-card, ../hooks/use-instance-actions, ../state/sessions
|
- SessionsPage.tsx | Renders a sessions management page that displays, polls health for, and handles CRUD operations on development environment sessions with dirty delete confirmation. | exp: SessionsPage | dep: react, ../api/sessions, ../api/settings, ../components/data-states, ../components/features/session/session-list, ../components/features/session/session-card, ../hooks/use-instance-actions, ../state/sessions
|
||||||
- SettingsPage.tsx | A React settings page component that loads, displays, and manages user configuration with tabbed navigation and nested outlet for child routes. | exp: SettingsPage | dep: react, react-router-dom, ../api/settings, ../components/data-states, ../hooks/use-async-data, ../components/features/settings/GeneralSettingsTab
|
- SettingsPage.tsx | A React settings page component that loads, displays, and manages user configuration with tabbed navigation and nested outlet for child routes. | exp: SettingsPage | dep: react, react-router-dom, ../api/settings, ../components/data-states, ../hooks/use-async-data, ../components/features/settings/GeneralSettingsTab
|
||||||
- SshKeysPage.tsx | React page component for managing SSH keys including generation, listing, signing, verification, and deletion | exp: SSHKeysPage | dep: react-router-dom, ../components/data-states, ../hooks/use-ssh-keys, ../components/features/ssh-keys/SSHKeyCreateForm, ../components/features/ssh-keys/SSHKeyList
|
- SshKeysPage.tsx | React page component for managing SSH keys including generation, listing, signing, verification, and deletion | exp: SSHKeysPage | dep: react-router-dom, ../components/data-states, ../hooks/use-ssh-keys, ../components/features/ssh-keys/SSHKeyCreateForm, ../components/features/ssh-keys/SSHKeyList
|
||||||
- TerminalPage.tsx | Renders a responsive terminal page that switches between mobile and desktop views based on device type, managing terminal sessions and their interactions. | exp: TerminalPage | dep: react, ../hooks/use-terminal-page, ../components/features/terminal/MobileTerminalView, ../components/features/terminal/DesktopTerminalView, useTerminalPage hook, MobileTerminalView, DesktopTerminalView
|
- TerminalPage.tsx | Renders a responsive terminal page that switches between mobile and desktop views based on device type, managing terminal sessions and their interactions. | exp: TerminalPage | dep: react, ../hooks/use-terminal-page, ../components/features/terminal/MobileTerminalView, ../components/features/terminal/DesktopTerminalView, useTerminalPage hook, MobileTerminalView, DesktopTerminalView
|
||||||
- ToolWorkshopPage.tsx | Renders a responsive tool workshop page with list and editor views for managing tool types, handling loading/error states and mobile/desktop layouts | exp: ToolWorkshopPage | dep: ../components/data-states, ../hooks/use-mobile-viewport, ../hooks/use-tool-workshop, ../components/features/tool-workshop/ToolTypeListSidebar, ../components/features/tool-workshop/ToolTypeEditorPanel, ../components/features/tool-workshop/ToolWorkshopMobileView, react, use-mobile-viewport, use-tool-workshop, data-states, ToolTypeListSidebar, ToolTypeEditorPanel, ToolWorkshopMobileView
|
- ToolWorkshopPage.tsx | Renders a responsive tool workshop page with sidebar/editor layout for desktop and tabbed mobile view for managing tool types | exp: ToolWorkshopPage | dep: ../components/data-states, ../hooks/use-mobile-viewport, ../hooks/use-tool-workshop, ../components/features/tool-workshop/ToolTypeListSidebar, ../components/features/tool-workshop/ToolTypeEditorPanel, ../components/features/tool-workshop/ToolWorkshopMobileView, react, use-mobile-viewport, use-tool-workshop, data-states, ToolTypeListSidebar, ToolTypeEditorPanel, ToolWorkshopMobileView
|
||||||
- WorkspaceDetailPage.test.tsx | Tests the WorkspaceDetailPage component rendering and tab switching behavior | dep: @testing-library/jest-dom/vitest, @testing-library/react, react-router-dom, vitest, ./WorkspaceDetailPage, @testing-library/jest-dom, WorkspaceDetailPage, use-workspaces, use-workspace-files, use-workspace-git, use-workspace-instances, use-mobile-viewport
|
- WorkspaceDetailPage.test.tsx | Tests the WorkspaceDetailPage component rendering and tab switching behavior | dep: @testing-library/jest-dom/vitest, @testing-library/react, react-router-dom, vitest, ./WorkspaceDetailPage, @testing-library/jest-dom, WorkspaceDetailPage, use-workspaces, use-workspace-files, use-workspace-git, use-workspace-instances, use-mobile-viewport
|
||||||
- WorkspaceDetailPage.tsx | Renders a workspace detail page with tab-based navigation for files, git, tools, and settings panels, with mobile-responsive layout. | exp: func:WorkspaceDetailPage(), call:useParams, call:useState, call:useMobileViewport, call:useWorkspaces, call:workspaces.find | dep: react, react-router-dom, ../hooks/use-workspaces, ../hooks/use-mobile-viewport, ../components/features/workspace/workspace-detail-header, ../components/features/workspace/workspace-tab-bar, ../components/features/workspace/workspace-file-panel, ../components/features/workspace/workspace-git-panel, ../components/features/workspace/workspace-tools-panel, ../components/features/workspace/workspace-settings-panel, use-workspaces, use-mobile-viewport, workspace-detail-header, workspace-tab-bar, workspace-file-panel, workspace-git-panel, workspace-tools-panel, workspace-settings-panel
|
- WorkspaceDetailPage.tsx | Renders a workspace detail page with tab-based navigation for files, git, tools, and settings panels, with mobile-responsive layout. | exp: func:WorkspaceDetailPage(), call:useParams, call:useState, call:useMobileViewport, call:useWorkspaces, call:workspaces.find | dep: react, react-router-dom, ../hooks/use-workspaces, ../hooks/use-mobile-viewport, ../components/features/workspace/workspace-detail-header, ../components/features/workspace/workspace-tab-bar, ../components/features/workspace/workspace-file-panel, ../components/features/workspace/workspace-git-panel, ../components/features/workspace/workspace-tools-panel, ../components/features/workspace/workspace-settings-panel, use-workspaces, use-mobile-viewport, workspace-detail-header, workspace-tab-bar, workspace-file-panel, workspace-git-panel, workspace-tools-panel, workspace-settings-panel
|
||||||
- WorkspacesPage.tsx | Renders a responsive workspaces management page with mobile and desktop views supporting listing, creating, viewing details, syncing, deleting, and starting tools for workspaces. | exp: func:WorkspacesPage(), call:useMobileViewport, call:useState, call:useWorkspaces, call:useWorkspaceActions, call:actions.delete, call:actions.sync, call:setMobileView, call:refresh, call:setStartWorkspace, call:handleDelete, call:setSelectedWorkspace, call:workspaces.map, call:e.stopPropagation, call:setShowCreate | dep: react, ../components/icon, ../hooks/use-mobile-viewport, ../hooks/use-workspaces, ../hooks/use-workspace-actions, ../components/features/workspace/workspace-card, ../components/features/workspace/workspace-create-form, ../components/features/mobile/mobile-detail-view, ../components/features/mobile/mobile-fab, ../components/features/tool/tool-starter, ../types/workspace
|
- WorkspacesPage.tsx | Renders a responsive workspaces management page with separate mobile and desktop layouts, supporting workspace listing, creation, deletion, sync, and tool launching. | exp: func:WorkspacesPage(), call:useMobileViewport, call:useState, call:useWorkspaces, call:useWorkspaceActions, call:actions.delete, call:actions.sync, call:setMobileView, call:refresh, call:setStartWorkspace, call:handleDelete, call:setSelectedWorkspace, call:workspaces.map, call:e.stopPropagation, call:setShowCreate | dep: react, ../components/icon, ../hooks/use-mobile-viewport, ../hooks/use-workspaces, ../hooks/use-workspace-actions, ../components/features/workspace/workspace-card, ../components/features/workspace/workspace-create-form, ../components/features/mobile/mobile-detail-view, ../components/features/mobile/mobile-fab, ../components/features/tool/tool-starter, ../types/workspace
|
||||||
## arch
|
## arch
|
||||||
Route-based page organization with responsive mobile/desktop layout patterns, tabbed navigation for complex pages, polling/health-check mechanisms for live data, and CRUD operations with loading/error/empty states throughout.
|
Follows a page-based routing architecture where each file maps to a URL route, using responsive design patterns with explicit mobile/desktop view branching, compound component layouts (sidebar/editor, tabbed panels), polling for real-time data, and direct API integration within page components rather than abstracted service layers.
|
||||||
## tags
|
## tags
|
||||||
page, components, workspace, features, mobile, react, hooks, settings
|
page, components, workspace, features, react, mobile, settings, hooks
|
||||||
## symbols
|
## symbols
|
||||||
- WorkspaceDetailPage
|
- WorkspaceDetailPage
|
||||||
- WorkspacesPage
|
- WorkspacesPage
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ConfigProfileListSidebar } from "../components/features/config-profiles
|
|||||||
import { ConfigProfileEditorPanel } from "../components/features/config-profiles/ConfigProfileEditorPanel";
|
import { ConfigProfileEditorPanel } from "../components/features/config-profiles/ConfigProfileEditorPanel";
|
||||||
import { ConfigProfilesMobileView } from "../components/features/config-profiles/ConfigProfilesMobileView";
|
import { ConfigProfilesMobileView } from "../components/features/config-profiles/ConfigProfilesMobileView";
|
||||||
|
|
||||||
type MobileView = "list" | "detail" | "edit" | "preview";
|
type MobileView = "list" | "detail" | "edit";
|
||||||
|
|
||||||
export const ConfigProfilesPage = () => {
|
export const ConfigProfilesPage = () => {
|
||||||
const isMobile = useMobileViewport();
|
const isMobile = useMobileViewport();
|
||||||
@@ -100,18 +100,11 @@ export const ConfigProfilesPage = () => {
|
|||||||
return (
|
return (
|
||||||
<ConfigProfilesMobileView
|
<ConfigProfilesMobileView
|
||||||
profiles={profiles}
|
profiles={profiles}
|
||||||
projects={projects}
|
|
||||||
toolTypes={toolTypes}
|
|
||||||
selectedProfile={selectedProfile}
|
selectedProfile={selectedProfile}
|
||||||
mobileView={mobileView}
|
mobileView={mobileView}
|
||||||
isCreating={isCreating}
|
isCreating={isCreating}
|
||||||
formData={formData}
|
formData={formData}
|
||||||
includedProfileIds={includedProfileIds}
|
|
||||||
availableProfiles={availableProfilesForInclude()}
|
|
||||||
previewData={previewData}
|
|
||||||
previewingId={previewingId}
|
|
||||||
saveStatus={saveStatus}
|
saveStatus={saveStatus}
|
||||||
error={error}
|
|
||||||
onViewChange={setMobileView}
|
onViewChange={setMobileView}
|
||||||
onSelect={handleSelectProfile}
|
onSelect={handleSelectProfile}
|
||||||
onCreate={handleCreateNew}
|
onCreate={handleCreateNew}
|
||||||
@@ -119,23 +112,6 @@ export const ConfigProfilesPage = () => {
|
|||||||
onFormChange={updateFormField}
|
onFormChange={updateFormField}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
getScopeLabel={getScopeLabel}
|
getScopeLabel={getScopeLabel}
|
||||||
getIncludedProfile={getIncludedProfile}
|
|
||||||
onAddInclude={addInclude}
|
|
||||||
onRemoveInclude={removeInclude}
|
|
||||||
onAddEnvVar={addEnvVar}
|
|
||||||
onUpdateEnvVar={updateEnvVar}
|
|
||||||
onRemoveEnvVar={removeEnvVar}
|
|
||||||
onAddFile={addFile}
|
|
||||||
onUpdateFile={updateFile}
|
|
||||||
onRemoveFile={removeFile}
|
|
||||||
onAddMount={addMount}
|
|
||||||
onUpdateMount={updateMount}
|
|
||||||
onRemoveMount={removeMount}
|
|
||||||
onAddMountFile={addMountFile}
|
|
||||||
onUpdateMountFile={updateMountFile}
|
|
||||||
onRemoveMountFile={removeMountFile}
|
|
||||||
onPreview={handlePreview}
|
|
||||||
onClosePreview={() => setPreviewData(null)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,13 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|||||||
import { getProfile, updateProfile, uploadAvatar } from "../api/profile";
|
import { getProfile, updateProfile, uploadAvatar } from "../api/profile";
|
||||||
import { ErrorState, LoadingState } from "../components/data-states";
|
import { ErrorState, LoadingState } from "../components/data-states";
|
||||||
import { Icon } from "../components/icon";
|
import { Icon } from "../components/icon";
|
||||||
import { ProfileMobileView } from "../components/features/profile/ProfileMobileView";
|
|
||||||
import { useAuth } from "../state/auth";
|
import { useAuth } from "../state/auth";
|
||||||
import { useAsyncData } from "../hooks/use-async-data";
|
import { useAsyncData } from "../hooks/use-async-data";
|
||||||
import { useMobileViewport } from "../hooks/use-mobile-viewport";
|
|
||||||
import type { UserProfile } from "../api/profile";
|
import type { UserProfile } from "../api/profile";
|
||||||
|
|
||||||
type ProfileStatus = "loading" | "ready" | "error" | "saving";
|
type ProfileStatus = "loading" | "ready" | "error" | "saving";
|
||||||
|
|
||||||
export const ProfilePage = () => {
|
export const ProfilePage = () => {
|
||||||
const isMobile = useMobileViewport();
|
|
||||||
const { refreshSession } = useAuth();
|
const { refreshSession } = useAuth();
|
||||||
const { data: profile, status: loadStatus, reload } = useAsyncData<UserProfile>(getProfile, []);
|
const { data: profile, status: loadStatus, reload } = useAsyncData<UserProfile>(getProfile, []);
|
||||||
const [displayStatus, setDisplayStatus] = useState<ProfileStatus>("loading");
|
const [displayStatus, setDisplayStatus] = useState<ProfileStatus>("loading");
|
||||||
@@ -93,30 +90,14 @@ export const ProfilePage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="stack">
|
<section className="stack">
|
||||||
{!isMobile && <h1>Profile</h1>}
|
<h1>Profile</h1>
|
||||||
|
|
||||||
{displayStatus === "loading" && <LoadingState message="Loading profile..." />}
|
{displayStatus === "loading" && <LoadingState message="Loading profile..." />}
|
||||||
|
|
||||||
{displayStatus === "error" && <ErrorState message="Failed to load profile" onRetry={reload} />}
|
{displayStatus === "error" && <ErrorState message="Failed to load profile" onRetry={reload} />}
|
||||||
|
|
||||||
{(displayStatus === "ready" || displayStatus === "saving") && profile && (
|
{(displayStatus === "ready" || displayStatus === "saving") && profile && (
|
||||||
isMobile ? (
|
<div className="card stack">
|
||||||
<ProfileMobileView
|
|
||||||
profile={profile}
|
|
||||||
name={name}
|
|
||||||
email={email}
|
|
||||||
error={error}
|
|
||||||
isSaving={displayStatus === "saving"}
|
|
||||||
avatarUrl={avatarUrl}
|
|
||||||
fileInputRef={fileInputRef}
|
|
||||||
onNameChange={setName}
|
|
||||||
onEmailChange={setEmail}
|
|
||||||
onAvatarButtonClick={() => fileInputRef.current?.click()}
|
|
||||||
onAvatarChange={handleAvatarChange}
|
|
||||||
onSave={() => void handleSave()}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="card stack">
|
|
||||||
<div className="profile-avatar-section">
|
<div className="profile-avatar-section">
|
||||||
<div className="avatar-preview">
|
<div className="avatar-preview">
|
||||||
{avatarUrl ? (
|
{avatarUrl ? (
|
||||||
@@ -197,7 +178,7 @@ export const ProfilePage = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -57,20 +57,12 @@ export const ToolWorkshopPage = () => {
|
|||||||
mobileView={mobileView}
|
mobileView={mobileView}
|
||||||
isCreating={isCreating}
|
isCreating={isCreating}
|
||||||
toolTypeForm={toolTypeForm}
|
toolTypeForm={toolTypeForm}
|
||||||
manifestData={manifestData}
|
|
||||||
manifestDefinitionId={manifestDefinitionId}
|
|
||||||
baseDefinitions={baseDefinitions}
|
|
||||||
toolTypeError={toolTypeError}
|
toolTypeError={toolTypeError}
|
||||||
toolTypeDirty={toolTypeDirty}
|
|
||||||
onViewChange={setMobileView}
|
onViewChange={setMobileView}
|
||||||
onSelect={handleSelectToolType}
|
onSelect={handleSelectToolType}
|
||||||
onCreate={handleCreateNew}
|
onCreate={handleCreateNew}
|
||||||
onDelete={handleDeleteToolType}
|
onDelete={handleDeleteToolType}
|
||||||
onFormChange={handleFormChange}
|
onFormChange={handleFormChange}
|
||||||
onManifestChange={(m) => {
|
|
||||||
setManifestData(m);
|
|
||||||
setToolTypeDirty(true);
|
|
||||||
}}
|
|
||||||
onSubmit={handleToolTypeSubmit}
|
onSubmit={handleToolTypeSubmit}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
if (toolTypeDirty) {
|
if (toolTypeDirty) {
|
||||||
@@ -78,7 +70,7 @@ export const ToolWorkshopPage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setMobileView("list");
|
setMobileView(isCreating ? "list" : "detail");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/styles
|
dir: apps/web/src/styles
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides the complete visual design system and styling foundation for the web application, including themes, tokens, utilities, and component-specific styles.
|
Provides the complete visual design system and styling foundation for the web application, encompassing global styles, design tokens, utility classes, and component-specific styles.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
map: apps/web/src/.pi-map.md
|
map: apps/web/src/.pi-map.md
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ dir: apps/web/src/styles
|
|||||||
index: apps/web/src/styles/.pi-map.index.md
|
index: apps/web/src/styles/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides the complete visual design system and styling foundation for the web application, including themes, tokens, utilities, and component-specific styles.
|
Provides the complete visual design system and styling foundation for the web application, encompassing global styles, design tokens, utility classes, and component-specific styles.
|
||||||
## files
|
## files
|
||||||
- global.css | Defines global CSS styles for a web application shell layout, navigation, cards, forms, dialogs, settings pages, and responsive design patterns. | dep: CSS custom properties (CSS variables: --border, --panel, --brand, --muted, --ink, --bg, --danger, --success, --warning, --font-size-xs, --font-size-sm, --space-2, --space-3, --space-4, --space-5)
|
- global.css | Defines global CSS styles for a web application shell layout, navigation, cards, forms, dialogs, settings pages, and responsive design patterns. | dep: CSS custom properties (CSS variables: --border, --panel, --brand, --muted, --ink, --bg, --danger, --success, --warning, --font-size-xs, --font-size-sm, --space-2, --space-3, --space-4, --space-5)
|
||||||
- syntax-highlight.css | Stylesheet for a syntax highlighting component with toolbar, line numbers, code display, and Prism.js theme integration | dep: Prism.js
|
- syntax-highlight.css | Stylesheet for a syntax highlighting component with toolbar, line numbers, code display, and Prism.js theme integration | dep: Prism.js
|
||||||
- tokens.css | Defines a comprehensive CSS design token system with light/dark themes, spacing scales, breakpoints, and fluid typography for a web application.
|
- tokens.css | Defines a comprehensive CSS design token system with light/dark themes, spacing scales, breakpoints, and fluid typography for a web application.
|
||||||
- utilities.css | Provides responsive CSS utility classes and component styles for a web application including layout, terminal, session navigation, and mobile-specific adaptations. | dep: CSS custom properties (--space-*, --border, --bg, --muted, --brand, --danger, --danger-light, --success, --text, --text-xs, --text-sm), xterm.js (terminal library)
|
- utilities.css | Provides responsive CSS utility classes and component-specific styles for a web application featuring terminals, dialogs, tables, forms, and navigation with mobile-first breakpoints | dep: CSS custom properties (variables like --space-*, --border, --bg, --brand, --muted, --success, --danger, --text-xs, --text-sm), xterm.js (terminal library)
|
||||||
## arch
|
## arch
|
||||||
Token-driven CSS architecture using CSS custom properties for light/dark theming, utility-first responsive patterns, and modular separation of concerns across global base styles, design tokens, syntax highlighting, and adaptive utilities.
|
CSS custom properties (variables) based theming system with light/dark mode support, mobile-first responsive breakpoints, fluid typography scales, utility-first class patterns, and modular separation of concerns across tokens, utilities, global layouts, and component-specific styles.
|
||||||
## tags
|
## tags
|
||||||
space, global, css, web, application, danger, syntax, text
|
space, global, css, web, application, syntax, defines, styles
|
||||||
## symbols
|
## symbols
|
||||||
-
|
-
|
||||||
## workflows
|
## workflows
|
||||||
|
|||||||
@@ -439,7 +439,6 @@ a.nav-item,
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-page-header {
|
.terminal-page-header {
|
||||||
@@ -458,7 +457,6 @@ a.nav-item,
|
|||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
max-height: 100%;
|
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -551,7 +549,6 @@ a.nav-item,
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -560,8 +557,6 @@ a.nav-item,
|
|||||||
.terminal-container .xterm-viewport {
|
.terminal-container .xterm-viewport {
|
||||||
touch-action: auto;
|
touch-action: auto;
|
||||||
overscroll-behavior: auto;
|
overscroll-behavior: auto;
|
||||||
max-height: 100% !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* xterm.js manages its own positioning and sizing */
|
/* xterm.js manages its own positioning and sizing */
|
||||||
@@ -772,9 +767,7 @@ a.nav-item,
|
|||||||
.terminal-instance {
|
.terminal-instance {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
max-height: 100%;
|
|
||||||
display: none;
|
display: none;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-instance.active {
|
.terminal-instance.active {
|
||||||
@@ -2060,38 +2053,10 @@ a.nav-item,
|
|||||||
.mobile-list-item-actions {
|
.mobile-list-item-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-1);
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-list-item-action {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
padding: 0;
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 8px;
|
|
||||||
color: var(--muted);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-list-item-action:active {
|
|
||||||
background: var(--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-list-item-action-delete {
|
|
||||||
color: var(--danger);
|
|
||||||
border-color: var(--danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-list-item-action-delete:active {
|
|
||||||
background: var(--danger-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-list-empty {
|
.mobile-list-empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -2144,198 +2109,6 @@ a.nav-item,
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile Page */
|
|
||||||
.mobile-page {
|
|
||||||
padding: var(--space-2);
|
|
||||||
padding-bottom: calc(var(--space-4) + 64px);
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile Edit View */
|
|
||||||
.mobile-edit-view {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100%;
|
|
||||||
background: var(--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-edit-header {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 10;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--space-2);
|
|
||||||
padding: var(--space-2) var(--space-3);
|
|
||||||
background: var(--panel);
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-edit-cancel,
|
|
||||||
.mobile-edit-save,
|
|
||||||
.mobile-edit-delete {
|
|
||||||
min-height: 44px;
|
|
||||||
padding: var(--space-2) var(--space-3);
|
|
||||||
border-radius: 10px;
|
|
||||||
font: inherit;
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-edit-cancel {
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-edit-save {
|
|
||||||
background: var(--brand);
|
|
||||||
border: 1px solid var(--brand);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-edit-delete {
|
|
||||||
background: var(--danger-light);
|
|
||||||
border: 1px solid var(--danger);
|
|
||||||
color: var(--danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-edit-save:disabled,
|
|
||||||
.mobile-edit-cancel:disabled,
|
|
||||||
.mobile-edit-delete:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-edit-title {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 0;
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-edit-form {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: var(--space-3);
|
|
||||||
padding-bottom: calc(var(--space-3) + 148px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile Edit Actions (sticky bottom bar) */
|
|
||||||
.mobile-edit-actions {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 64px;
|
|
||||||
z-index: 110;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-2);
|
|
||||||
padding: var(--space-3);
|
|
||||||
padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
|
|
||||||
background: var(--panel);
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-edit-actions .mobile-edit-save,
|
|
||||||
.mobile-edit-actions .mobile-edit-delete {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-edit-field {
|
|
||||||
margin-bottom: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-edit-field-label {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-edit-input,
|
|
||||||
.mobile-edit-textarea,
|
|
||||||
.mobile-edit-select {
|
|
||||||
width: 100%;
|
|
||||||
padding: var(--space-3);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--panel);
|
|
||||||
color: var(--text);
|
|
||||||
font: inherit;
|
|
||||||
min-height: 44px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-edit-textarea {
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 96px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-edit-code {
|
|
||||||
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
|
|
||||||
Consolas, monospace;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-edit-checkbox {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-edit-checkbox input {
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
accent-color: var(--brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile Profile */
|
|
||||||
.profile-mobile-avatar-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-3);
|
|
||||||
padding: var(--space-6) var(--space-3);
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-mobile-avatar {
|
|
||||||
width: 96px;
|
|
||||||
height: 96px;
|
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--bg);
|
|
||||||
border: 2px solid var(--border);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-mobile-avatar-image {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-mobile-avatar-placeholder {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--muted);
|
|
||||||
background: var(--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile Page Header */
|
/* Mobile Page Header */
|
||||||
.mobile-page-header {
|
.mobile-page-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2406,348 +2179,13 @@ a.nav-item,
|
|||||||
|
|
||||||
/* Mobile form group (inline fields) */
|
/* Mobile form group (inline fields) */
|
||||||
.mobile-form-group {
|
.mobile-form-group {
|
||||||
padding: var(--space-3);
|
|
||||||
background: var(--panel);
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-form-label {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-form-input,
|
|
||||||
.mobile-form-select,
|
|
||||||
.mobile-form-textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: var(--space-3);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
font: inherit;
|
|
||||||
min-height: 44px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-form-textarea {
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 96px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-form-code {
|
|
||||||
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
|
|
||||||
Consolas, monospace;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-form-select {
|
|
||||||
appearance: none;
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: right 0.75rem center;
|
|
||||||
padding-right: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-form-checkbox-label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-form-checkbox {
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
min-width: 22px;
|
|
||||||
min-height: 22px;
|
|
||||||
accent-color: var(--brand);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-form-help {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
color: var(--muted);
|
|
||||||
margin-top: var(--space-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-form-error {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
padding: var(--space-3);
|
|
||||||
background: var(--danger-light);
|
|
||||||
color: var(--danger);
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
margin: var(--space-2) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-discard-button {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Manifest editor responsive overrides inside mobile forms */
|
|
||||||
.mobile-manifest-editor-wrapper .card {
|
|
||||||
padding: var(--space-3);
|
|
||||||
margin-bottom: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-manifest-editor-wrapper .row {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-manifest-editor-wrapper .form-group,
|
|
||||||
.mobile-manifest-editor-wrapper input,
|
|
||||||
.mobile-manifest-editor-wrapper select,
|
|
||||||
.mobile-manifest-editor-wrapper textarea {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-manifest-editor-wrapper input[style*="width:"] {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile config profile form extras */
|
|
||||||
.mobile-form-group p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-form-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-form-section {
|
|
||||||
padding: var(--space-3);
|
|
||||||
background: var(--panel);
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-form-section-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-form-section-header h4 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-add-button {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-form-success {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
padding: var(--space-3);
|
|
||||||
background: var(--success-light);
|
|
||||||
color: var(--success);
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
margin: var(--space-2) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-list-plain {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-list-plain li {
|
|
||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
background: var(--bg);
|
background: var(--panel);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile detail extra actions */
|
|
||||||
.mobile-detail-actions-extra {
|
|
||||||
padding: var(--space-3);
|
|
||||||
padding-top: 0;
|
|
||||||
margin-top: calc(var(--space-2) * -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-detail-actions-extra .secondary-button {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile includes */
|
|
||||||
.mobile-includes-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-2);
|
|
||||||
margin-bottom: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-include-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
padding: var(--space-2) var(--space-3);
|
|
||||||
background: var(--bg);
|
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-include-name {
|
|
||||||
flex: 1;
|
|
||||||
font-weight: 500;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-include-scope {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
padding: 0.125rem 0.5rem;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--muted);
|
|
||||||
border-radius: 999px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.025em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile key/value rows */
|
|
||||||
.mobile-key-value-row {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-2);
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-key-value-row .mobile-form-input {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile file and mount cards */
|
|
||||||
.mobile-file-card,
|
|
||||||
.mobile-mount-card {
|
|
||||||
padding: var(--space-3);
|
|
||||||
background: var(--bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-file-header,
|
|
||||||
.mobile-mount-header {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-2);
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-file-header .mobile-form-input,
|
|
||||||
.mobile-mount-header .mobile-form-input {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-mount-files {
|
|
||||||
padding-left: var(--space-3);
|
|
||||||
border-left: 2px solid var(--border);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-mount-file-row {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-2);
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-mount-file-row .mobile-form-input,
|
|
||||||
.mobile-mount-file-row .mobile-form-textarea {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile preview */
|
|
||||||
.mobile-preview-code {
|
|
||||||
background: var(--code-bg, #1e1e1e);
|
|
||||||
color: var(--code-fg, #d4d4d4);
|
|
||||||
padding: var(--space-3);
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow-x: auto;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
|
|
||||||
Consolas, monospace;
|
|
||||||
margin: var(--space-2) 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-preview-item {
|
|
||||||
padding: var(--space-3);
|
|
||||||
background: var(--bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-preview-item strong {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-preview-mapping {
|
|
||||||
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
|
|
||||||
Consolas, monospace;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
color: var(--muted);
|
|
||||||
margin-top: var(--space-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile git mount editor overrides */
|
|
||||||
.mobile-form-section .git-mount-editor .card {
|
|
||||||
padding: var(--space-3);
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-form-section .git-mount-editor .form-row {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-form-section .git-mount-editor input,
|
|
||||||
.mobile-form-section .git-mount-editor select,
|
|
||||||
.mobile-form-section .git-mount-editor textarea {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-form-section .git-mount-editor .form-actions {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-form-section .git-mount-editor button {
|
|
||||||
min-height: 44px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure minimum touch targets on mobile */
|
/* Ensure minimum touch targets on mobile */
|
||||||
button,
|
button,
|
||||||
a,
|
a,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: docs
|
dir: docs
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Central documentation hub and navigation entry point for the Headquarter platform.
|
Provides user-facing documentation and knowledge base for the Headquarter platform.
|
||||||
## parent
|
## parent
|
||||||
index: ./.pi-map.index.md
|
index: ./.pi-map.index.md
|
||||||
map: ./.pi-map.md
|
map: ./.pi-map.md
|
||||||
|
|||||||
+2
-2
@@ -4,11 +4,11 @@ dir: docs
|
|||||||
index: docs/.pi-map.index.md
|
index: docs/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Central documentation hub and knowledge base for the Headquarter platform.
|
Provides user-facing documentation and knowledge base for the Headquarter platform.
|
||||||
## files
|
## files
|
||||||
- README.md | Serves as the main index and navigation hub for the Headquarter platform documentation, organizing links to feature guides, API docs, architecture, deployment, and development resources.
|
- README.md | Serves as the main index and navigation hub for the Headquarter platform documentation, organizing links to feature guides, API docs, architecture, deployment, and development resources.
|
||||||
## arch
|
## arch
|
||||||
Static documentation organization using hierarchical markdown files with cross-referenced navigation structure.
|
Static documentation site using markdown files organized hierarchically with a central README index.
|
||||||
## tags
|
## tags
|
||||||
readme, serves, main, index, navigation, hub, headquarter, platform
|
readme, serves, main, index, navigation, hub, headquarter, platform
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,14 +2,11 @@
|
|||||||
dir: docs/superpowers
|
dir: docs/superpowers
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Documentation for specialized capabilities or advanced features of the project.
|
Documentation directory for specialized or advanced capabilities/features of the project.
|
||||||
## parent
|
## parent
|
||||||
index: docs/.pi-map.index.md
|
index: docs/.pi-map.index.md
|
||||||
map: docs/.pi-map.md
|
map: docs/.pi-map.md
|
||||||
## children
|
## children
|
||||||
- docs/superpowers/handoffs
|
|
||||||
index: docs/superpowers/handoffs/.pi-map.index.md
|
|
||||||
map: docs/superpowers/handoffs/.pi-map.md
|
|
||||||
- docs/superpowers/plans
|
- docs/superpowers/plans
|
||||||
index: docs/superpowers/plans/.pi-map.index.md
|
index: docs/superpowers/plans/.pi-map.index.md
|
||||||
map: docs/superpowers/plans/.pi-map.md
|
map: docs/superpowers/plans/.pi-map.md
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ dir: docs/superpowers
|
|||||||
index: docs/superpowers/.pi-map.index.md
|
index: docs/superpowers/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Documentation for advanced or specialized features and capabilities of the project.
|
Documentation directory for specialized or advanced capabilities/features of the project.
|
||||||
## files
|
## files
|
||||||
## arch
|
## arch
|
||||||
Flat documentation structure with topic-based markdown files describing extended functionality beyond core features.
|
Static documentation storage with no executable code architecture; likely contains markdown or text files describing enhanced functionality.
|
||||||
## tags
|
## tags
|
||||||
-
|
-
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
# docs/superpowers/handoffs (index)
|
|
||||||
dir: docs/superpowers/handoffs
|
|
||||||
|
|
||||||
## role
|
|
||||||
Documents infrastructure migration procedures for development environment tooling.
|
|
||||||
## parent
|
|
||||||
index: docs/superpowers/.pi-map.index.md
|
|
||||||
map: docs/superpowers/.pi-map.md
|
|
||||||
## children
|
|
||||||
-
|
|
||||||
## files
|
|
||||||
- tool-container-home-directory.md
|
|
||||||
## links
|
|
||||||
index: docs/superpowers/handoffs/.pi-map.index.md
|
|
||||||
map: docs/superpowers/handoffs/.pi-map.md
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# docs/superpowers/handoffs
|
|
||||||
dir: docs/superpowers/handoffs
|
|
||||||
|
|
||||||
index: docs/superpowers/handoffs/.pi-map.index.md
|
|
||||||
|
|
||||||
## role
|
|
||||||
Documents infrastructure migration procedures for development environment tooling.
|
|
||||||
## files
|
|
||||||
- tool-container-home-directory.md | Documents a multi-phase implementation plan for changing tool container home directories from `/workspace` to `/home/user` while preserving repo root directory names in mount targets and maintaining backward compatibility. | dep: alembic, docker, python, pydantic/sqlalchemy models, manifest compiler, instance service, config profile resolver, permission fixer
|
|
||||||
## arch
|
|
||||||
Technical documentation following phased implementation pattern with backward compatibility considerations.
|
|
||||||
## tags
|
|
||||||
home, tool, container, directory, documents, multi, phase, implementation
|
|
||||||
## symbols
|
|
||||||
-
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# Handoff: Tool Container Home Directory
|
|
||||||
|
|
||||||
## Session context
|
|
||||||
|
|
||||||
- Original request: make tool container workspace/home directory default to `/home/user`, keep it user-writable, and preserve repo root directory name in the mount target.
|
|
||||||
- Completed a Grill Me session that resolved design decisions.
|
|
||||||
- Phase 1 implementation was started but blocked by persistent Grill Me read-only guard.
|
|
||||||
|
|
||||||
## Current branch
|
|
||||||
|
|
||||||
- `feat/tool-container-home-directory` (created from `dev`)
|
|
||||||
|
|
||||||
## Committed artifacts on `dev`
|
|
||||||
|
|
||||||
- `docs: tool container home directory design, plan, and test plan`
|
|
||||||
- `chore: update project maps for tool container home directory artifacts`
|
|
||||||
|
|
||||||
## Files already created/edited on `feat/tool-container-home-directory`
|
|
||||||
|
|
||||||
- `apps/api/src/models/tool/tool_type.py`
|
|
||||||
- Added `home_directory: Mapped[str]` column, default `"/home/user"`.
|
|
||||||
- `apps/api/alembic/versions/2026_06_14_104415_add_tool_type_home_directory.py`
|
|
||||||
- Adds column, sets existing rows to `/home/user`.
|
|
||||||
- Rewrites `/workspace` to `/home/user/{{WORKSPACE_NAME}}` in `compose_template` and `dockerfile_template`.
|
|
||||||
- Provides reversible downgrade.
|
|
||||||
|
|
||||||
## Design decisions resolved
|
|
||||||
|
|
||||||
| Area | Decision |
|
|
||||||
|------|----------|
|
|
||||||
| Config surface | `home_directory` field on ToolType / manifest schema |
|
|
||||||
| Default | `/home/user` |
|
|
||||||
| Runtime user | Still `manifest.user.name` |
|
|
||||||
| Mount target | `{home_directory}/{repo_name}` or `{home_directory}/{workspace_name}` |
|
|
||||||
| Precedence | Explicit manifest repo mount target wins; `home_directory` fills in default |
|
|
||||||
| Migration | Alembic data migration for legacy ToolType templates |
|
|
||||||
| Compatibility | `/workspace` symlink to actual repo/workspace target |
|
|
||||||
| Permissions | Entrypoint permission fixer at container startup |
|
|
||||||
| Tests | Unit + integration tests |
|
|
||||||
|
|
||||||
## Pending work
|
|
||||||
|
|
||||||
### Phase 1 verification
|
|
||||||
Run in `apps/api`:
|
|
||||||
```bash
|
|
||||||
python -m alembic current
|
|
||||||
python -m alembic upgrade head
|
|
||||||
python -m alembic downgrade -1
|
|
||||||
python -m alembic upgrade head
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 2: Manifest compiler
|
|
||||||
File: `apps/api/src/services/build/manifest_compiler.py`
|
|
||||||
- Honor `manifest.home_directory` in `get_manifest_home_dir()`.
|
|
||||||
- Set `ENV HOME={home_directory}`, `ENV USER={user.name}`, `WORKDIR {home_directory}`.
|
|
||||||
- Create `/workspace` symlink step.
|
|
||||||
- Use `{home_directory}/{repo_name}` as default repo mount target when no explicit repo mount exists.
|
|
||||||
|
|
||||||
### Phase 3: Legacy instance generation
|
|
||||||
File: `apps/api/src/services/tool/instance_service.py`
|
|
||||||
- Use `tool_type.home_directory` for `dockerfile` tool mounts.
|
|
||||||
- Add `WORKSPACE_NAME`/`HOME_DIRECTORY` template variables for compose tools.
|
|
||||||
|
|
||||||
### Phase 4: Config-profile / git mount expansion
|
|
||||||
Files: `apps/api/src/services/tool/instance_service.py`, `apps/api/src/services/config/config_profile_resolver.py`
|
|
||||||
- Thread resolved `home_dir` through startup.
|
|
||||||
|
|
||||||
### Phase 5: Entrypoint permission fixer
|
|
||||||
Files: `apps/api/src/services/build/manifest_compiler.py`, `apps/api/src/services/shared/permission_fixer.py`, `tool-images/base.dockerfile`
|
|
||||||
- Generate startup script that chowns `{home_directory}` and key mounts to container user.
|
|
||||||
|
|
||||||
### Phase 6: Tests
|
|
||||||
Create/expand:
|
|
||||||
- `apps/api/tests/unit/test_home_path_expansion.py`
|
|
||||||
- `apps/api/tests/unit/test_manifest_compiler.py`
|
|
||||||
- `apps/api/tests/unit/test_instance_service.py`
|
|
||||||
- `apps/api/tests/unit/test_alembic_migrations.py`
|
|
||||||
- `apps/api/tests/integration/test_tool_instance_lifecycle.py`
|
|
||||||
|
|
||||||
## Reference docs
|
|
||||||
|
|
||||||
- `openspec/designs/tool-container-home-directory.md`
|
|
||||||
- `docs/superpowers/plans/tool-container-home-directory.md`
|
|
||||||
- `docs/superpowers/specs/tool-container-home-directory-test-plan.md`
|
|
||||||
- `openspec/tasks/home-path-expansion.md`
|
|
||||||
|
|
||||||
## Next action for fresh session
|
|
||||||
|
|
||||||
1. Ensure you are on `feat/tool-container-home-directory`.
|
|
||||||
2. Verify Phase 1 migration round-trip.
|
|
||||||
3. Continue with Phase 2 (manifest compiler).
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: docs/superpowers/plans
|
dir: docs/superpowers/plans
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains dated implementation planning documents that specify step-by-step technical approaches for major infrastructure, authentication, database, and UI feature development.
|
Contains implementation planning documents for core infrastructure, authentication, database modeling, and UI feature development.
|
||||||
## parent
|
## parent
|
||||||
index: docs/superpowers/.pi-map.index.md
|
index: docs/superpowers/.pi-map.index.md
|
||||||
map: docs/superpowers/.pi-map.md
|
map: docs/superpowers/.pi-map.md
|
||||||
@@ -13,7 +13,6 @@ map: docs/superpowers/.pi-map.md
|
|||||||
- 2026-05-17-auth-oauth.md
|
- 2026-05-17-auth-oauth.md
|
||||||
- 2026-05-17-database-models.md
|
- 2026-05-17-database-models.md
|
||||||
- 2026-05-24-session-branch-selection.md
|
- 2026-05-24-session-branch-selection.md
|
||||||
- tool-container-home-directory.md
|
|
||||||
## links
|
## links
|
||||||
index: docs/superpowers/plans/.pi-map.index.md
|
index: docs/superpowers/plans/.pi-map.index.md
|
||||||
map: docs/superpowers/plans/.pi-map.md
|
map: docs/superpowers/plans/.pi-map.md
|
||||||
|
|||||||
@@ -4,17 +4,16 @@ dir: docs/superpowers/plans
|
|||||||
index: docs/superpowers/plans/.pi-map.index.md
|
index: docs/superpowers/plans/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains dated implementation planning documents that specify step-by-step technical approaches for major infrastructure, authentication, database, and UI feature development.
|
Contains implementation planning documents for core infrastructure, authentication, database modeling, and UI feature development.
|
||||||
## files
|
## files
|
||||||
- 2024-05-16-docker-infrastructure.md | Defines a step-by-step implementation plan for setting up a complete Docker-based development infrastructure with multi-service orchestration including PostgreSQL, Redis, Traefik, Authentik, FastAPI backend, and React frontend. | dep: Docker, Docker Compose, Make, Python 3.11, Node.js 20, PostgreSQL, Redis, Traefik, Authentik, FastAPI, React, Vite, nginx, alembic, uvicorn, SQLAlchemy, pydantic, structlog
|
- 2024-05-16-docker-infrastructure.md | Defines a step-by-step implementation plan for setting up a complete Docker-based development infrastructure with multi-service orchestration including PostgreSQL, Redis, Traefik, Authentik, FastAPI backend, and React frontend. | dep: Docker, Docker Compose, Make, Python 3.11, Node.js 20, PostgreSQL, Redis, Traefik, Authentik, FastAPI, React, Vite, nginx, alembic, uvicorn, SQLAlchemy, pydantic, structlog
|
||||||
- 2026-05-17-auth-oauth.md | Implementation plan for adding Authentik-backed OIDC authentication with JWT access tokens and DB-backed refresh tokens to a FastAPI application | dep: FastAPI, SQLAlchemy, Alembic, python-jose, httpx, pytest, Authentik, PostgreSQL
|
- 2026-05-17-auth-oauth.md | Implementation plan for adding Authentik-backed OIDC authentication with JWT access tokens and DB-backed refresh tokens to a FastAPI application | dep: FastAPI, SQLAlchemy, Alembic, python-jose, httpx, pytest, Authentik, PostgreSQL
|
||||||
- 2026-05-17-database-models.md | Define an implementation plan for building SQLAlchemy 2.0 async database models, Alembic migrations, and seed data for a FastAPI backend. | dep: SQLAlchemy 2.x, asyncpg, Alembic, pytest, pytest-asyncio, Pydantic Settings, PostgreSQL, FastAPI
|
- 2026-05-17-database-models.md | Define an implementation plan for building SQLAlchemy 2.0 async database models, Alembic migrations, and seed data for a FastAPI backend. | dep: SQLAlchemy 2.x, asyncpg, Alembic, pytest, pytest-asyncio, Pydantic Settings, PostgreSQL, FastAPI
|
||||||
- 2026-05-24-session-branch-selection.md | Implementation plan for replacing free-text branch input with a dropdown of available branches and adding "Create new branch" functionality in a session creation form | dep: React, TypeScript, FastAPI, Python, Git subprocess, Pydantic, API client
|
- 2026-05-24-session-branch-selection.md | Implementation plan for replacing free-text branch input with a dropdown of available branches and adding "Create new branch" functionality in a session creation form | dep: React, TypeScript, FastAPI, Python, Git subprocess, Pydantic, API client
|
||||||
- tool-container-home-directory.md | Implementation plan to make `/home/user` the configurable default home directory for tool containers while preserving repository name in mount targets, migrating legacy tool types, and maintaining `/workspace` backward compatibility symlink. | dep: Alembic, Docker, Compose, manifest compiler, instance service, config profile resolver, permission fixer, base dockerfile, pytest
|
|
||||||
## arch
|
## arch
|
||||||
Document-driven planning architecture using chronological Markdown specifications with precise technical scope, dependencies, and migration strategies for incremental system evolution.
|
Markdown-based technical specification documents using date-prefixed naming convention, each detailing phased implementation steps for specific system capabilities with technology stack integration points.
|
||||||
## tags
|
## tags
|
||||||
05, fastapi, 2026, implementation, plan, docker, alembic, postgresql
|
05, fastapi, 2026, implementation, plan, docker, postgresql, authentik
|
||||||
## symbols
|
## symbols
|
||||||
-
|
-
|
||||||
## workflows
|
## workflows
|
||||||
|
|||||||
@@ -1,171 +0,0 @@
|
|||||||
# Implementation Plan: Tool Container Home Directory
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Make `/home/user` the configurable, default workspace/home directory for all tool containers, preserve the repository/workspace directory name in the mount target, migrate legacy tool types, and keep `/workspace` as a compatibility symlink.
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
|
|
||||||
- Add a `home_directory` field to `ToolType` and the manifest schema.
|
|
||||||
- Use that field to control workspace mount target, `HOME`, `WORKDIR`, and `~`/`$HOME` expansion.
|
|
||||||
- Preserve `{repo_name}` / `{workspace_name}` in the mount target.
|
|
||||||
- Migrate legacy Dockerfile/Compose templates via Alembic.
|
|
||||||
- Keep `/workspace` symlink for backward compatibility.
|
|
||||||
- Add an entrypoint permission fixer for runtime-mounted paths.
|
|
||||||
- Validate with unit and integration tests.
|
|
||||||
|
|
||||||
## Phases
|
|
||||||
|
|
||||||
### Phase 1: Database and model changes
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- `apps/api/src/models/tool/tool_type.py`
|
|
||||||
- `apps/api/alembic/versions/<new>_add_tool_type_home_directory.py`
|
|
||||||
|
|
||||||
**Tasks:**
|
|
||||||
1. Add `home_directory: Mapped[str]` column to `ToolType`, non-nullable with server default `"/home/user"`.
|
|
||||||
2. Create Alembic migration that:
|
|
||||||
- Adds the column.
|
|
||||||
- Updates existing rows to `/home/user`.
|
|
||||||
- Rewrites `compose_template` and `dockerfile_template` to replace `/workspace` with `/home/user/{{WORKSPACE_NAME}}` (or a compatible template variable).
|
|
||||||
3. Provide downgrade that reverses template rewrites and drops the column.
|
|
||||||
|
|
||||||
**Quality gate:**
|
|
||||||
- `cd apps/api && alembic upgrade head` succeeds.
|
|
||||||
- `alembic downgrade -1` succeeds and restores `/workspace` strings.
|
|
||||||
- Existing tests still pass.
|
|
||||||
|
|
||||||
### Phase 2: Manifest schema and compiler
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- `apps/api/src/services/build/manifest_compiler.py`
|
|
||||||
- `apps/api/src/services/tool/instance_service.py`
|
|
||||||
- `apps/api/src/api/tool/tool_types_validation.py` (if schema validation is added)
|
|
||||||
|
|
||||||
**Tasks:**
|
|
||||||
1. Accept `home_directory` in the manifest schema (optional; fallback to ToolType value).
|
|
||||||
2. In `compile_dockerfile()`:
|
|
||||||
- Set `ENV HOME={home_directory}` and `ENV USER={user.name}`.
|
|
||||||
- Set `WORKDIR {home_directory}` (unless `runtime.working_dir` is present).
|
|
||||||
- Create the home directory and pre-create `.config`, `.local/share`, `.cache` under it.
|
|
||||||
- Add a step to create `/workspace` as a symlink to `{home_directory}/{repo_name}` (placeholder or startup-time).
|
|
||||||
3. In `compile_compose()`:
|
|
||||||
- Use `{home_directory}/{repo_name}` as the default repo mount target when the manifest has no explicit repo mount.
|
|
||||||
- Keep `~`/`$HOME` expansion base equal to `home_directory`.
|
|
||||||
4. Update `get_manifest_home_dir()` to honor `manifest.home_directory` before deriving from `user.name`.
|
|
||||||
5. Pass `home_directory` through the manifest-based instance lifecycle.
|
|
||||||
|
|
||||||
**Quality gate:**
|
|
||||||
- `compile_dockerfile()` output contains `ENV HOME=/home/user` and `WORKDIR /home/user` for default manifests.
|
|
||||||
- `compile_compose()` output mounts repo at `/home/user/{repo_name}` when no explicit repo mount exists.
|
|
||||||
|
|
||||||
### Phase 3: Legacy instance generation
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- `apps/api/src/services/tool/instance_service.py`
|
|
||||||
- `apps/api/src/services/docker/compose.py`
|
|
||||||
|
|
||||||
**Tasks:**
|
|
||||||
1. In `create_tool_instance()` for `definition_type == "dockerfile"`:
|
|
||||||
- Read `tool_type.home_directory` (default `/home/user`).
|
|
||||||
- Mount `{repo_path}:{home_directory}/{repo_name}` instead of `{repo_path}:/workspace`.
|
|
||||||
- Generate or adjust Dockerfile/Compose to create `/workspace` symlink.
|
|
||||||
2. For `definition_type == "compose"`:
|
|
||||||
- Render `{home_directory}` and `{WORKSPACE_NAME}` into the template.
|
|
||||||
- Validate that `{WORKSPACE_NAME}` is available as a template variable.
|
|
||||||
3. Add `WORKSPACE_NAME` to the render variables in `render_compose_template()`.
|
|
||||||
|
|
||||||
**Quality gate:**
|
|
||||||
- Legacy `dockerfile` instance compose mounts repo at `/home/user/{repo_name}`.
|
|
||||||
- Legacy `compose` template with `/home/user/{{WORKSPACE_NAME}}` renders correctly.
|
|
||||||
|
|
||||||
### Phase 4: Config-profile and git mount expansion
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- `apps/api/src/services/config/config_profile_resolver.py`
|
|
||||||
- `apps/api/src/services/tool/instance_service.py`
|
|
||||||
|
|
||||||
**Tasks:**
|
|
||||||
1. Ensure `expand_container_path()` uses the resolved `home_directory` (already present; verify it is threaded through).
|
|
||||||
2. In `start_tool_instance()`, compute `home_dir` from ToolType/manifest and pass it to:
|
|
||||||
- `apply_resolved_profile()`
|
|
||||||
- `resolve_git_mounts()` / `resolve_git_mount_mappings()`
|
|
||||||
3. Confirm workspace/repo name is used as the mount target, not a generic `workspace` string.
|
|
||||||
|
|
||||||
**Quality gate:**
|
|
||||||
- Config profile mount target `~/config` expands to `/home/user/config`.
|
|
||||||
- Git mount target `~/repo` expands to `/home/user/repo`.
|
|
||||||
|
|
||||||
### Phase 5: Entrypoint permission fixer
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- `apps/api/src/services/build/manifest_compiler.py`
|
|
||||||
- `apps/api/src/services/shared/permission_fixer.py`
|
|
||||||
- `tool-images/base.dockerfile` or generated entrypoint
|
|
||||||
|
|
||||||
**Tasks:**
|
|
||||||
1. Generate an entrypoint script that, before switching to the runtime user:
|
|
||||||
- Detects the container user name/uid.
|
|
||||||
- Runs `chown` on `{home_directory}` and key mount points.
|
|
||||||
- Creates `/workspace` symlink if it does not yet exist.
|
|
||||||
- Avoids recursive chown of large subtrees; target top-level dirs and runtime-created files.
|
|
||||||
2. Ensure manifest-generated Dockerfiles install `sudo` and configure passwordless sudo for the runtime user (already partially done).
|
|
||||||
3. Consider updating `tool-images/base.dockerfile` to include `sudo` and an entrypoint hook, or keep the fixer entirely in generated images.
|
|
||||||
|
|
||||||
**Quality gate:**
|
|
||||||
- Container starts successfully.
|
|
||||||
- Container user can write to `{home_directory}` and mounted config/git directories.
|
|
||||||
- `/workspace` symlink resolves to the repo/workspace directory.
|
|
||||||
|
|
||||||
### Phase 6: Tests
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- `apps/api/tests/unit/test_home_path_expansion.py`
|
|
||||||
- `apps/api/tests/unit/test_manifest_compiler.py`
|
|
||||||
- `apps/api/tests/unit/test_instance_service.py` (new or expanded)
|
|
||||||
- `apps/api/tests/integration/test_tool_instance_lifecycle.py` (new or expanded)
|
|
||||||
|
|
||||||
**Tasks:**
|
|
||||||
1. Unit tests:
|
|
||||||
- `expand_container_path` with `~`, `$HOME`, absolute, and relative paths.
|
|
||||||
- `get_manifest_home_dir` with and without `home_directory`, with and without `user.name`.
|
|
||||||
- `compile_dockerfile` includes correct `ENV HOME`, `WORKDIR`, and `/workspace` symlink step.
|
|
||||||
- `compile_compose` defaults repo mount to `/home/user/{repo_name}` when no explicit repo mount exists.
|
|
||||||
- Explicit manifest repo mount target is preserved.
|
|
||||||
2. Integration tests:
|
|
||||||
- Create a manifest tool instance and verify the running container has `HOME=/home/user`, repo at `/home/user/{repo_name}`, and `/workspace` symlink.
|
|
||||||
- Verify a config-profile mount under `~` is writable by the container user.
|
|
||||||
- Verify git mount under `~` is writable.
|
|
||||||
- Verify legacy dockerfile tool uses `/home/user/{repo_name}` after migration.
|
|
||||||
|
|
||||||
**Quality gate:**
|
|
||||||
- `cd apps/api && pytest tests/unit/... tests/integration/... -xvs` passes.
|
|
||||||
- `cd apps/web && npm run typecheck` passes.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- The existing `home-path-expansion` helpers (`expand_container_path`) must already be in place.
|
|
||||||
- Alembic migration infrastructure must be working.
|
|
||||||
- Manifest compiler must support generated entrypoints (already partially implemented).
|
|
||||||
|
|
||||||
## Risks and Mitigations
|
|
||||||
|
|
||||||
| Risk | Mitigation |
|
|
||||||
|------|-----------|
|
|
||||||
| Alembic migration rewrites commands/env vars that contain `/workspace` | Scope the replacement to volume mount lines only; add tests for edge cases. |
|
|
||||||
| `/workspace` symlink target missing at build time | Create placeholder directory in Dockerfile; finalize symlink in entrypoint at startup. |
|
|
||||||
| Entrypoint chown slow on large repos | Chown top-level directories only; rely on runtime user for new files. |
|
|
||||||
| Base image `USER user` cannot run sudo | Either install `sudo` in base image or keep entrypoint running as root before `su`/`gosu` to runtime user. |
|
|
||||||
| Legacy templates without `{{WORKSPACE_NAME}}` break | Migration injects the variable; validation rejects missing required variables. |
|
|
||||||
|
|
||||||
## OpenSpec Task Update
|
|
||||||
|
|
||||||
After this plan is accepted, update or replace `openspec/tasks/home-path-expansion.md` to reflect the expanded scope (configurable `home_directory`, repo-name mount target, migration, compatibility symlink, permission fixer, tests).
|
|
||||||
|
|
||||||
## Rollout
|
|
||||||
|
|
||||||
1. Merge Phase 1 (schema + migration) first so the column exists.
|
|
||||||
2. Merge Phase 2 and 3 (compiler + legacy generation) next.
|
|
||||||
3. Merge Phase 4 and 5 (profile/git mounts + permission fixer).
|
|
||||||
4. Merge Phase 6 (tests) with the previous phases as appropriate.
|
|
||||||
5. Run migration in production during a maintenance window; validate a few existing tool types before full rollout.
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: docs/superpowers/specs
|
dir: docs/superpowers/specs
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains design specifications and test plans for upcoming features ("superpowers") being planned for the project.
|
Contains design specification documents for planned system features, serving as architectural decision records for authentication and session management capabilities.
|
||||||
## parent
|
## parent
|
||||||
index: docs/superpowers/.pi-map.index.md
|
index: docs/superpowers/.pi-map.index.md
|
||||||
map: docs/superpowers/.pi-map.md
|
map: docs/superpowers/.pi-map.md
|
||||||
@@ -11,7 +11,6 @@ map: docs/superpowers/.pi-map.md
|
|||||||
## files
|
## files
|
||||||
- 2026-05-17-auth-oauth-design.md
|
- 2026-05-17-auth-oauth-design.md
|
||||||
- 2026-05-24-session-branch-selection-design.md
|
- 2026-05-24-session-branch-selection-design.md
|
||||||
- tool-container-home-directory-test-plan.md
|
|
||||||
## links
|
## links
|
||||||
index: docs/superpowers/specs/.pi-map.index.md
|
index: docs/superpowers/specs/.pi-map.index.md
|
||||||
map: docs/superpowers/specs/.pi-map.md
|
map: docs/superpowers/specs/.pi-map.md
|
||||||
|
|||||||
@@ -4,15 +4,14 @@ dir: docs/superpowers/specs
|
|||||||
index: docs/superpowers/specs/.pi-map.index.md
|
index: docs/superpowers/specs/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains design specifications and test plans for upcoming features ("superpowers") being planned for the project.
|
Contains design specification documents for planned system features, serving as architectural decision records for authentication and session management capabilities.
|
||||||
## files
|
## files
|
||||||
- 2026-05-17-auth-oauth-design.md | Design document for implementing OAuth2/OIDC authentication with Authentik, internal JWT access tokens, and database-backed refresh token rotation. | dep: Authentik, JWKS, JWT, database/PostgreSQL, cookies, pytest, mypy, ruff
|
- 2026-05-17-auth-oauth-design.md | Design document for implementing OAuth2/OIDC authentication with Authentik, internal JWT access tokens, and database-backed refresh token rotation. | dep: Authentik, JWKS, JWT, database/PostgreSQL, cookies, pytest, mypy, ruff
|
||||||
- 2026-05-24-session-branch-selection-design.md | Design document for replacing free-text branch input with a dropdown of available branches and adding the ability to create a new local branch at clone time in a session creation UI. | dep: React/TypeScript frontend, Python/FastAPI backend, Git, API client, ToolInstance database model
|
- 2026-05-24-session-branch-selection-design.md | Design document for replacing free-text branch input with a dropdown of available branches and adding the ability to create a new local branch at clone time in a session creation UI. | dep: React/TypeScript frontend, Python/FastAPI backend, Git, API client, ToolInstance database model
|
||||||
- tool-container-home-directory-test-plan.md | Test plan and QA checklist for validating configurable home directory behavior in tool containers, covering manifest compilation, path expansion, database migrations, and mount target resolution. | dep: pytest, alembic, docker, compose, manifest compiler, instance service, ToolType, config profiles, git mounts
|
|
||||||
## arch
|
## arch
|
||||||
Document-driven architecture using dated Markdown design documents for feature specifications, plus standalone test plans for QA validation of complex features.
|
Markdown-based technical design documentation following a chronological naming convention (YYYY-MM-DD-feature-name-design.md) with no executable code, using a lightweight specification format for capturing requirements, decisions, and implementation plans before development.
|
||||||
## tags
|
## tags
|
||||||
design, 2026, 05, branch, database, session, test, plan
|
design, 2026, 05, branch, session, document, authentik, jwt
|
||||||
## symbols
|
## symbols
|
||||||
-
|
-
|
||||||
## workflows
|
## workflows
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
# Test Plan / QA Checklist: Tool Container Home Directory
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This plan covers validation of the configurable `home_directory` feature for tool containers, including:
|
|
||||||
|
|
||||||
- `home_directory` field on `ToolType` and manifest schema.
|
|
||||||
- Repository/workspace mount target using `{home_directory}/{repo_name}` or `{home_directory}/{workspace_name}`.
|
|
||||||
- Manifest compiler behavior (`HOME`, `WORKDIR`, `/workspace` symlink).
|
|
||||||
- Legacy tool type migration via Alembic.
|
|
||||||
- Config-profile and git-mount `~`/`$HOME` expansion.
|
|
||||||
- Entrypoint permission fixer.
|
|
||||||
|
|
||||||
## Unit Tests
|
|
||||||
|
|
||||||
### `apps/api/tests/unit/test_home_path_expansion.py`
|
|
||||||
|
|
||||||
| ID | Test | Expected Result |
|
|
||||||
|----|------|-----------------|
|
|
||||||
| U1 | `expand_container_path("~/config", "/home/user")` | returns `/home/user/config` |
|
|
||||||
| U2 | `expand_container_path("~", "/home/user")` | returns `/home/user` |
|
|
||||||
| U3 | `expand_container_path("$HOME/config", "/home/user")` | returns `/home/user/config` |
|
|
||||||
| U4 | `expand_container_path("$HOME", "/home/user")` | returns `/home/user` |
|
|
||||||
| U5 | `expand_container_path("/opt/data", "/home/user")` | returns `/opt/data` unchanged |
|
|
||||||
| U6 | `expand_container_path("relative/path", "/home/user")` | returns `relative/path` unchanged |
|
|
||||||
| U7 | `expand_container_path("path/$HOME/other", "/home/user")` | returns `path/$HOME/other` unchanged (mid-string not expanded) |
|
|
||||||
|
|
||||||
### `apps/api/tests/unit/test_manifest_compiler.py`
|
|
||||||
|
|
||||||
| ID | Test | Expected Result |
|
|
||||||
|----|------|-----------------|
|
|
||||||
| U10 | `get_manifest_home_dir({"home_directory": "/home/dev"})` | returns `/home/dev` |
|
|
||||||
| U11 | `get_manifest_home_dir({"user": {"name": "user"}})` | returns `/home/user` |
|
|
||||||
| U12 | `get_manifest_home_dir({"user": {"name": "dev"}})` | returns `/home/dev` |
|
|
||||||
| U13 | `get_manifest_home_dir({})` | returns `/root` |
|
|
||||||
| U14 | `compile_dockerfile()` for manifest with `home_directory: /home/user` and `user.name: user` | Dockerfile contains `ENV HOME=/home/user`, `ENV USER=user`, `WORKDIR /home/user`, and creates `/workspace` symlink |
|
|
||||||
| U15 | `compile_dockerfile()` for manifest without user block | Dockerfile does not create user and defaults `HOME=/root` |
|
|
||||||
| U16 | `compile_compose()` with no explicit repo mount and `home_directory: /home/user` | Compose mounts `{repo_path}:/home/user/{repo_name}` |
|
|
||||||
| U17 | `compile_compose()` with explicit repo mount target `/custom/path` | Compose preserves `/custom/path`; `/home/user` default is not injected |
|
|
||||||
|
|
||||||
### `apps/api/tests/unit/test_instance_service.py` (new or expanded)
|
|
||||||
|
|
||||||
| ID | Test | Expected Result |
|
|
||||||
|----|------|-----------------|
|
|
||||||
| U20 | `create_tool_instance()` for `definition_type == "dockerfile"` | Compose mounts `{repo_path}:/home/user/{repo_name}` |
|
|
||||||
| U21 | `create_tool_instance()` for `definition_type == "compose"` with migrated template | Template renders `/home/user/{{WORKSPACE_NAME}}` correctly |
|
|
||||||
| U22 | `start_tool_instance()` passes `home_dir` to `apply_resolved_profile()` | Config-profile `~/config` expands to `/home/user/config` |
|
|
||||||
| U23 | `start_tool_instance()` passes `home_dir` to `resolve_git_mounts()` | Git mount `~/repo` expands to `/home/user/repo` |
|
|
||||||
|
|
||||||
### `apps/api/tests/unit/test_alembic_migrations.py` (new or expanded)
|
|
||||||
|
|
||||||
| ID | Test | Expected Result |
|
|
||||||
|----|------|-----------------|
|
|
||||||
| U30 | Upgrade adds `home_directory` column with default `/home/user` | Column exists and legacy rows have value `/home/user` |
|
|
||||||
| U31 | Upgrade rewrites `compose_template` containing `/workspace` | Template now uses `/home/user/{{WORKSPACE_NAME}}` |
|
|
||||||
| U32 | Downgrade restores `/workspace` strings | Original templates are restored |
|
|
||||||
| U33 | Downgrade drops `home_directory` column | Column no longer exists |
|
|
||||||
|
|
||||||
## Integration Tests
|
|
||||||
|
|
||||||
### `apps/api/tests/integration/test_tool_instance_lifecycle.py` (new or expanded)
|
|
||||||
|
|
||||||
| ID | Test | Steps | Expected Result |
|
|
||||||
|----|------|-------|-----------------|
|
|
||||||
| I1 | Manifest tool starts with `/home/user` home | Create manifest tool type with default `home_directory`; create and start instance | Container has `HOME=/home/user`, repo at `/home/user/{repo_name}`, `/workspace` symlink resolves correctly |
|
|
||||||
| I2 | Manifest tool with overridden `home_directory` | Set `home_directory: /home/dev` in manifest; create and start instance | Container has `HOME=/home/dev`, repo at `/home/dev/{repo_name}` |
|
|
||||||
| I3 | Container user can write under `/home/user` | Start instance; exec `touch /home/user/test-file` as container user | Command succeeds and file is owned by container user |
|
|
||||||
| I4 | Config-profile mount under `~` is writable | Create profile with mount `{"target": "~/config", "files": {"settings.json": "{}"}}`; start instance with profile | File appears at `/home/user/config/settings.json` and is writable by container user |
|
|
||||||
| I5 | Git mount under `~` is writable | Create profile with git mount mapping `{"target_path": "~/dotfiles"}`; start instance | Repo files appear at `/home/user/dotfiles` and are writable by container user |
|
|
||||||
| I6 | Legacy dockerfile tool uses new mount target after migration | Run Alembic migration; create legacy dockerfile tool instance | Repo mounts at `/home/user/{repo_name}` and `/workspace` symlink works |
|
|
||||||
| I7 | Explicit manifest repo mount target is preserved | Create manifest with explicit repo mount target `/opt/repo`; start instance | Repo mounts at `/opt/repo`; no `/home/user/{repo_name}` mount injected |
|
|
||||||
|
|
||||||
## Manual QA Checklist
|
|
||||||
|
|
||||||
- [ ] Create a new manifest tool type without specifying `home_directory`; verify default `/home/user` is used.
|
|
||||||
- [ ] Create a manifest tool type with `home_directory: /home/custom`; verify container reflects the override.
|
|
||||||
- [ ] Verify repository root directory name appears in the mount target (e.g., `/home/user/my-app`).
|
|
||||||
- [ ] Verify workspace-based instance uses workspace name in the mount target.
|
|
||||||
- [ ] Verify `/workspace` symlink points to the actual repo/workspace directory and `cd /workspace && pwd` works.
|
|
||||||
- [ ] Verify legacy dockerfile tool, after migration, mounts repo under `/home/user/{repo_name}`.
|
|
||||||
- [ ] Verify config-profile file mount under `~/config` lands at `/home/user/config`.
|
|
||||||
- [ ] Verify git mount under `~/dotfiles` lands at `/home/user/dotfiles`.
|
|
||||||
- [ ] Verify container user can create files under `/home/user` and mounted config directories.
|
|
||||||
- [ ] Verify `alembic downgrade -1` restores `/workspace` strings and removes the column.
|
|
||||||
|
|
||||||
## Regression Coverage
|
|
||||||
|
|
||||||
- Existing manifest tools without `home_directory` continue to work (default `/home/user`).
|
|
||||||
- Existing config profiles without `~`/`$HOME` continue to work.
|
|
||||||
- Existing legacy tools that did not use `/workspace` are unaffected by the migration rewrite.
|
|
||||||
- Existing `home-path-expansion` tests still pass.
|
|
||||||
|
|
||||||
## Test Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/api
|
|
||||||
pytest tests/unit/test_home_path_expansion.py tests/unit/test_manifest_compiler.py tests/unit/test_instance_service.py tests/unit/test_alembic_migrations.py -xvs
|
|
||||||
pytest tests/integration/test_tool_instance_lifecycle.py -xvs
|
|
||||||
|
|
||||||
# Alembic round-trip
|
|
||||||
alembic upgrade head
|
|
||||||
alembic downgrade -1
|
|
||||||
alembic upgrade head
|
|
||||||
|
|
||||||
cd apps/web
|
|
||||||
npm run typecheck
|
|
||||||
```
|
|
||||||
|
|
||||||
## Exit Criteria
|
|
||||||
|
|
||||||
- All listed unit and integration tests pass.
|
|
||||||
- Manual QA checklist is completed or equivalent automated coverage is added.
|
|
||||||
- Alembic migration round-trip passes without errors.
|
|
||||||
- No regressions in existing `home-path-expansion` or tool instance tests.
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: openspec
|
dir: openspec
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Defines the OpenSpec methodology and configuration for managing software requirements, specifications, and task tracking as living documentation within a Docker-based coding agent management platform.
|
Defines the OpenSpec methodology and configuration for managing living documentation, requirements, and task tracking within a Docker-based coding agent platform.
|
||||||
## parent
|
## parent
|
||||||
index: ./.pi-map.index.md
|
index: ./.pi-map.index.md
|
||||||
map: ./.pi-map.md
|
map: ./.pi-map.md
|
||||||
|
|||||||
+2
-2
@@ -4,12 +4,12 @@ dir: openspec
|
|||||||
index: openspec/.pi-map.index.md
|
index: openspec/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Defines the OpenSpec methodology and configuration for managing software requirements, specifications, and task tracking as living documentation within a Docker-based coding agent management platform.
|
Defines the OpenSpec methodology and configuration for managing living documentation, requirements, and task tracking within a Docker-based coding agent platform.
|
||||||
## files
|
## files
|
||||||
- README.md | Documents the OpenSpec methodology for managing software requirements, specifications, and task tracking as living documentation within a project repository. | dep: OpenSpec CLI (@fission-ai/openspec), Docker, SQLAlchemy, Alembic, Authentik, React, TypeScript, Tailwind, Traefik, Jinja2, xterm.js, pytest, mypy, ruff, npm
|
- README.md | Documents the OpenSpec methodology for managing software requirements, specifications, and task tracking as living documentation within a project repository. | dep: OpenSpec CLI (@fission-ai/openspec), Docker, SQLAlchemy, Alembic, Authentik, React, TypeScript, Tailwind, Traefik, Jinja2, xterm.js, pytest, mypy, ruff, npm
|
||||||
- config.yaml | Configuration file defining project metadata, technology stack, and software development discipline rules for a Docker-based coding agent management platform | dep: FastAPI, React, Vite, PostgreSQL, SQLAlchemy, Redis, Alembic, pytest, Docker Compose, Traefik, Authentik
|
- config.yaml | Configuration file defining project metadata, technology stack, and software development discipline rules for a Docker-based coding agent management platform | dep: FastAPI, React, Vite, PostgreSQL, SQLAlchemy, Redis, Alembic, pytest, Docker Compose, Traefik, Authentik
|
||||||
## arch
|
## arch
|
||||||
Documentation-as-code pattern with YAML-based configuration management, structured metadata definitions, and rule-based discipline enforcement for AI-assisted software development workflows.
|
Documentation-as-code pattern with YAML-driven configuration, embedding specifications directly in the repository rather than external tools, supporting automated agent workflows.
|
||||||
## tags
|
## tags
|
||||||
software, project, docker, sqlalchemy, alembic, authentik, react, traefik
|
software, project, docker, sqlalchemy, alembic, authentik, react, traefik
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
# Fix Terminal Container Overflow on Desktop
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
On the desktop Terminal page, the terminal container sometimes grows taller than its allocated flex/grid track and acquires its own scrollbar, which in turn causes the outer page to scroll. This happens because the flex container chain from `.terminal-page` down to `.terminal-container` is missing `min-height: 0`, allowing a child with intrinsic content height to expand its parent.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
- `apps/web/src/styles/utilities.css`
|
|
||||||
- `.terminal-page`
|
|
||||||
- `.terminal-page-content`
|
|
||||||
- `.terminal-instance`
|
|
||||||
- `.terminal-wrapper`
|
|
||||||
- `.terminal-container`
|
|
||||||
- `.xterm-viewport`
|
|
||||||
|
|
||||||
## Root Cause
|
|
||||||
|
|
||||||
`.terminal-page-content` is a flex item that receives its height from the flex parent. In CSS flexbox, a flex item's `min-height` defaults to `auto`, which is based on its content. If the xterm.js viewport/canvas renders taller than the available track, the flex item expands to fit it, pushing the page beyond `100vh` and creating a page-level scrollbar.
|
|
||||||
|
|
||||||
## Fix
|
|
||||||
|
|
||||||
1. Add `min-height: 0` to every flex/grid item in the terminal layout chain so each level respects the constrained height from its parent.
|
|
||||||
2. Keep `overflow: hidden` on the wrapper/container so xterm.js's internal viewport is the only scroll surface.
|
|
||||||
3. Ensure `.terminal-container .xterm-viewport` fills the container and does not expand past it.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
- [x] Terminal page never shows an outer vertical scrollbar due to terminal content.
|
|
||||||
- [x] Terminal wrapper and container stay within their allocated track.
|
|
||||||
- [x] xterm.js internal viewport still scrolls normally.
|
|
||||||
- [x] Mobile terminal layout is unaffected.
|
|
||||||
- [x] Quality gates pass: `npm run typecheck`, `npm run lint`, `npm test -- --run`.
|
|
||||||
|
|
||||||
## Related
|
|
||||||
|
|
||||||
- `openspec/specs/tool-terminal`
|
|
||||||
- `openspec/changes/fix-tmux-mouse-config`
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Fix Terminal Container Overflow on Desktop — Tasks
|
|
||||||
|
|
||||||
- [x] Discard debug `terminal.tsx` changes from `debug/mobile-terminal-scroll`.
|
|
||||||
- [x] Create focused branch `fix/terminal-container-overflow` from `dev`.
|
|
||||||
- [x] Update `utilities.css` to prevent terminal container from overflowing its flex/grid track:
|
|
||||||
- [x] `.terminal-page`: add `overflow: hidden`.
|
|
||||||
- [x] `.terminal-instance`: add `max-height: 100%` and `overflow: hidden`.
|
|
||||||
- [x] `.terminal-wrapper`: add `max-height: 100%`.
|
|
||||||
- [x] `.terminal-container`: add `min-height: 0`.
|
|
||||||
- [x] `.terminal-container .xterm-viewport`: add `max-height: 100% !important` and `width: 100% !important`.
|
|
||||||
- [x] Run `npm run typecheck` and `npm run lint` in `apps/web`.
|
|
||||||
- [x] Run `npm test -- --run` in `apps/web` (87 passed).
|
|
||||||
- [x] Update project maps for changed files.
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# Fix tmux Mouse Mode Configuration in Pi Agent Images
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
The Pi Agent dockerfile templates create `/home/user/.tmux.conf` using `echo '... \n ...'` with single quotes. In both Dockerfile `RUN` commands and Python-embedded dockerfile strings, single quotes prevent `\n` from being interpreted as a newline, so the resulting `.tmux.conf` contains a single malformed line such as:
|
|
||||||
|
|
||||||
```
|
|
||||||
set -g mouse on\nset -g default-terminal "screen-256color"
|
|
||||||
```
|
|
||||||
|
|
||||||
Tmux fails to parse this, so `set -g mouse on` is never applied. Without tmux mouse mode, xterm.js sends mouse-wheel events as Up/Down arrow keys to the shell, causing wheel scroll to cycle command history instead of scrolling the terminal/tmux buffer.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
- `tool-images/pi-agent.dockerfile`
|
|
||||||
- `apps/api/alembic/versions/20260527160017_add_pi_agent_tool_type.py`
|
|
||||||
- `apps/api/alembic/versions/2026_05_28_add_tool_definition_manifests.py`
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
- [x] Pi Agent image builds write a valid multi-line `.tmux.conf`.
|
|
||||||
- [x] `.tmux.conf` contains `set -g mouse on` on its own line.
|
|
||||||
- [x] `.tmux.conf` contains `set -g default-terminal "screen-256color"` on its own line.
|
|
||||||
- [x] Ranger config is also fixed where the same literal `\n` bug exists.
|
|
||||||
- [x] Alembic migration dockerfile strings produce the same valid commands as the standalone Dockerfile.
|
|
||||||
|
|
||||||
## Related
|
|
||||||
|
|
||||||
- `openspec/specs/tool-terminal`
|
|
||||||
- `tool-images/pi-agent.dockerfile`
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# Fix tmux Mouse Mode Configuration in Pi Agent Images — Tasks
|
|
||||||
|
|
||||||
- [x] Identify the literal `\n` bug in `.tmux.conf` creation commands across Pi Agent dockerfiles and migrations.
|
|
||||||
- [x] Update `tool-images/pi-agent.dockerfile` to use `printf '%s\n'` for `.tmux.conf` and ranger `rc.conf`.
|
|
||||||
- [x] Update `apps/api/alembic/versions/20260527160017_add_pi_agent_tool_type.py` dockerfile string with the same fix.
|
|
||||||
- [x] Update `apps/api/alembic/versions/2026_05_28_add_tool_definition_manifests.py` dockerfile string with the same fix.
|
|
||||||
- [x] Verify resulting Dockerfile commands produce valid multi-line config files.
|
|
||||||
- [x] Run `npm run typecheck` and `npm run lint` in `apps/web`.
|
|
||||||
- [x] Run `npm test -- --run` in `apps/web` (87 passed).
|
|
||||||
- [x] Verify Python migration files compile with `py_compile`.
|
|
||||||
- [x] Update project maps for changed files.
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# Mobile Config Profiles UI Rework
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Redesign the mobile **Config Profiles** detail ("preview") and edit pages to match the full desktop functionality, following the same mobile UX pattern used for the Tool Workshop.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
- Rewrite `ConfigProfilesMobileView` so the detail view displays all profile fields and supports resolving/previewing the profile.
|
|
||||||
- Rewrite the mobile edit view to expose every field available on the desktop editor:
|
|
||||||
- name, description, project, tool type, default flag
|
|
||||||
- includes with add/remove/reorder
|
|
||||||
- environment variables
|
|
||||||
- runtime hints
|
|
||||||
- files
|
|
||||||
- mounts with nested files
|
|
||||||
- git mounts
|
|
||||||
- Update `ConfigProfilesPage` to pass the required state and callbacks.
|
|
||||||
- Update `useConfigProfiles` so `handleSubmit` returns success status for cleaner mobile navigation.
|
|
||||||
- Add mobile-specific CSS for config profile forms.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
- On viewports narrower than 768 px, the Config Profiles page renders a mobile-friendly list/detail/edit flow.
|
|
||||||
- The mobile detail view shows all profile fields and offers a Preview action that displays the resolved profile.
|
|
||||||
- The mobile edit view supports all desktop fields, including includes, runtime hints, files, mounts, nested mount files, and git mounts.
|
|
||||||
- All existing desktop functionality remains intact.
|
|
||||||
- `npm run typecheck` and `npm run lint` pass in `apps/web`.
|
|
||||||
|
|
||||||
## Out of Scope
|
|
||||||
|
|
||||||
- Backend API changes.
|
|
||||||
- Changes to other pages.
|
|
||||||
- New features beyond existing desktop capabilities.
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# Tasks: Mobile Config Profiles UI Rework
|
|
||||||
|
|
||||||
- [x] Inspect existing ConfigProfilesMobileView, desktop editor, hook, and page.
|
|
||||||
- [x] Update `useConfigProfiles.handleSubmit` to return boolean success.
|
|
||||||
- [x] Rewrite `ConfigProfilesMobileView` detail view with full field display and preview action.
|
|
||||||
- [x] Rewrite `ConfigProfilesMobileView` edit view with all desktop fields (includes, env vars, runtime hints, files, mounts, git mounts).
|
|
||||||
- [x] Update `ConfigProfilesPage` to pass required props to mobile view.
|
|
||||||
- [x] Add responsive CSS for config profile mobile forms.
|
|
||||||
- [x] Run `npm run typecheck` and `npm run lint` in `apps/web` and fix issues.
|
|
||||||
- [x] Update project maps for changed files.
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Mobile Edit-as-Default with Sticky Bottom Actions
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
On mobile, tapping a config profile or tool type now opens the edit view directly instead of the read-only detail view. Save and Delete actions are anchored to a sticky bottom action bar so they are always reachable while scrolling long forms.
|
|
||||||
|
|
||||||
## Motivation
|
|
||||||
|
|
||||||
The previous mobile flow required an extra tap (list → detail → edit) for every change. Power users on phones primarily want to edit, so the default view should be edit. The Save and Delete buttons were also at the top of long forms, forcing users to scroll back up to commit changes.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
- Config Profiles mobile view (`ConfigProfilesMobileView`).
|
|
||||||
- Tool Workshop mobile view (`ToolWorkshopMobileView`).
|
|
||||||
- Shared `MobileEditView` component.
|
|
||||||
- Mobile edit-view CSS (`utilities.css`).
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
- [x] Tapping a config profile opens the edit form.
|
|
||||||
- [x] Tapping a tool type opens the edit form.
|
|
||||||
- [x] Create flows still open the edit form and return to the list after save.
|
|
||||||
- [x] Edit flows stay on the edit form after a successful save and show a success message.
|
|
||||||
- [x] Delete is available in the edit view for existing items.
|
|
||||||
- [x] Save and Delete buttons are sticky at the bottom of the viewport on mobile.
|
|
||||||
- [x] Detail and preview views remain reachable from edit where already wired.
|
|
||||||
|
|
||||||
## Non-Goals
|
|
||||||
|
|
||||||
- Changing desktop behavior or layout.
|
|
||||||
- Removing detail/preview views entirely.
|
|
||||||
- Altering the user profile page flow (it already uses `MobileEditView` and benefits from the sticky bar automatically).
|
|
||||||
|
|
||||||
## Related
|
|
||||||
|
|
||||||
- `openspec/changes/mobile-config-profiles-ui`
|
|
||||||
- `openspec/changes/mobile-tool-profile-ui`
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Mobile Edit-as-Default with Sticky Bottom Actions — Tasks
|
|
||||||
|
|
||||||
- [x] Update `MobileEditView` to accept an optional `onDelete` callback and render Save/Delete in a sticky bottom action bar.
|
|
||||||
- [x] Move the Save button out of the `MobileEditView` header; keep Cancel + title in the header.
|
|
||||||
- [x] Update `ConfigProfilesMobileView` so list item clicks open edit view.
|
|
||||||
- [x] Update `ConfigProfilesMobileView` to pass `onDelete` to `MobileEditView` when editing existing profiles.
|
|
||||||
- [x] Update `ConfigProfilesMobileView` cancel/save navigation to return to list on create and stay on edit on save.
|
|
||||||
- [x] Update `ToolWorkshopMobileView` so list item clicks open edit view.
|
|
||||||
- [x] Update `ToolWorkshopMobileView` to pass `onDelete` to `MobileEditView` when editing existing tool types.
|
|
||||||
- [x] Update `ToolWorkshopMobileView` cancel/save navigation to return to list on create and stay on edit on save.
|
|
||||||
- [x] Add CSS for `.mobile-edit-actions` sticky bottom bar and `.mobile-edit-delete` button.
|
|
||||||
- [x] Increase `.mobile-edit-form` bottom padding so content is not hidden behind the sticky bar.
|
|
||||||
- [x] Run `npm run typecheck`, `npm run lint`, and `npm test -- --run` in `apps/web`.
|
|
||||||
- [x] Update project maps for changed files.
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# Restore Delete Button in Mobile List Views and Fix Edit Action Bar
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
The `MobileListView` component accepted `onItemDelete` and `onItemDuplicate` callbacks but never rendered the corresponding action buttons. This change restores the delete button (and adds duplicate support) in each mobile list row for any view that supplies the callback.
|
|
||||||
|
|
||||||
In addition, the sticky bottom action bar added to `MobileEditView` in the previous change was positioned behind the 64px mobile navigation bar, making the Save and Delete buttons impossible to see on mobile. The bar is now raised above the navigation and given a higher z-index.
|
|
||||||
|
|
||||||
## Motivation
|
|
||||||
|
|
||||||
Users need a quick way to remove config profiles and tool types directly from the mobile list without first opening the edit view. The callback props already existed, so wiring them up restores intended behavior.
|
|
||||||
|
|
||||||
Users also reported that the Save/Delete buttons in the mobile edit view were not visible. Investigation showed the fixed-position action bar was rendering underneath the persistent mobile bottom navigation.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
- `MobileListView` shared component.
|
|
||||||
- `ToolWorkshopMobileView` (passes `onItemDelete`).
|
|
||||||
- `ConfigProfilesMobileView` already passed `onItemDelete`; it now actually renders.
|
|
||||||
- `MobileEditView` sticky action bar CSS (`utilities.css`).
|
|
||||||
- Mobile list item action CSS (`utilities.css`).
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
- [x] `MobileListView` renders a delete button when `onItemDelete` is provided.
|
|
||||||
- [x] `MobileListView` renders a duplicate button when `onItemDuplicate` is provided.
|
|
||||||
- [x] Tapping delete/duplicate does not also trigger the row click.
|
|
||||||
- [x] `ToolWorkshopMobileView` list rows show the delete button.
|
|
||||||
- [x] `ConfigProfilesMobileView` list rows show the delete button.
|
|
||||||
- [x] Rows without delete/duplicate still show the chevron.
|
|
||||||
- [x] The `MobileEditView` sticky bottom action bar sits above the mobile navigation bar.
|
|
||||||
- [x] Save and Delete buttons in the edit view are visible and reachable.
|
|
||||||
|
|
||||||
## Related
|
|
||||||
|
|
||||||
- `openspec/changes/mobile-edit-default-bottom-actions`
|
|
||||||
- `openspec/changes/mobile-config-profiles-ui`
|
|
||||||
- `openspec/changes/mobile-tool-profile-ui`
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# Restore Delete Button in Mobile List Views and Fix Edit Action Bar — Tasks
|
|
||||||
|
|
||||||
- [x] Read `MobileListView` source and confirm `onItemDelete`/`onItemDuplicate` were unused.
|
|
||||||
- [x] Update `MobileListView` to render delete/duplicate action buttons when callbacks are provided.
|
|
||||||
- [x] Stop propagation on action buttons so row click does not fire.
|
|
||||||
- [x] Update `ToolWorkshopMobileView` to pass `onItemDelete` to both list view usages.
|
|
||||||
- [x] Verify `ConfigProfilesMobileView` still passes `onItemDelete`.
|
|
||||||
- [x] Add `.mobile-list-item-action` and variant CSS to `utilities.css`.
|
|
||||||
- [x] Identify that `MobileEditView` sticky action bar was hidden behind the 64px mobile nav.
|
|
||||||
- [x] Update `.mobile-edit-actions` to `bottom: 64px` and `z-index: 110`.
|
|
||||||
- [x] Run `npm run typecheck`, `npm run lint`, and `npm test -- --run` in `apps/web`.
|
|
||||||
- [x] Update project maps for changed files.
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user