refactor: centralize types and extract seed data (Task 1.1)
- Create types/ directory with centralized domain types: session, tool-instance, tool-type, git-repository, config-folder, tool-config, project, user, api-response - Remove inline type definitions from API modules; re-export from types/ for backward compatibility - Update state/sessions.tsx to import Session from types/session.ts - Update all consumer components/pages to import from types/ - Extract seed_builtin_tool_types from main.py to seeds/builtin_tool_types.py - Create types/index.ts barrel export Quality gates: tsc (pass), eslint (pass), Python syntax (pass)
This commit is contained in:
+1
-152
@@ -7,8 +7,6 @@ from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from sqlalchemy import select, text
|
||||
|
||||
from src.api.auth import router as auth_router
|
||||
from src.api.dashboard import router as dashboard_router
|
||||
from src.api.git_repositories import router as git_repositories_router
|
||||
@@ -32,7 +30,7 @@ from src.logging_config import (
|
||||
RequestLoggingMiddleware,
|
||||
configure_logging,
|
||||
)
|
||||
from src.models.tool_type import ToolType
|
||||
from src.seeds.builtin_tool_types import seed_builtin_tool_types
|
||||
|
||||
# Configure logging early
|
||||
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
|
||||
@@ -104,155 +102,6 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
|
||||
)
|
||||
|
||||
|
||||
async def _table_exists(session, table_name: str) -> bool:
|
||||
"""Check if a table exists in the database."""
|
||||
try:
|
||||
result = await session.execute(
|
||||
text("""
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = :table_name
|
||||
)
|
||||
"""),
|
||||
{"table_name": table_name},
|
||||
)
|
||||
return result.scalar() or False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
async def seed_builtin_tool_types():
|
||||
async with SessionLocal() as session:
|
||||
# Check if tool_types table exists before attempting to seed
|
||||
if not await _table_exists(session, "tool_types"):
|
||||
logger.warning(
|
||||
"tool_types table does not exist. Skipping seeding. "
|
||||
"Migrations may not have run yet."
|
||||
)
|
||||
return
|
||||
|
||||
builtin_types = [
|
||||
{
|
||||
"name": "code-server",
|
||||
"display_name": "VS Code Server",
|
||||
"description": "VS Code running in the browser via code-server",
|
||||
"category": "editor",
|
||||
"interfaces": ["web"],
|
||||
"compose_template": """version: "3.8"
|
||||
services:
|
||||
code-server:
|
||||
image: lscr.io/linuxserver/code-server:latest
|
||||
container_name: {{TOOL_NAME}}
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/London
|
||||
volumes:
|
||||
- {{REPO_PATH}}:/config/workspace
|
||||
ports:
|
||||
- "8443:8443"
|
||||
restart: unless-stopped""",
|
||||
"default_port": 8443,
|
||||
"required_variables": ["REPO_PATH", "TOOL_NAME"],
|
||||
},
|
||||
{
|
||||
"name": "jupyter-notebook",
|
||||
"display_name": "Jupyter Notebook",
|
||||
"description": "Jupyter Lab for interactive development",
|
||||
"category": "notebook",
|
||||
"interfaces": ["web"],
|
||||
"default_port": 8888,
|
||||
"compose_template": """version: "3.8"
|
||||
services:
|
||||
jupyter:
|
||||
image: jupyter/scipy-notebook:latest
|
||||
container_name: {{TOOL_NAME}}
|
||||
environment:
|
||||
- JUPYTER_ENABLE_LAB=yes
|
||||
volumes:
|
||||
- {{REPO_PATH}}:/home/jovyan/work
|
||||
ports:
|
||||
- "8888:8888"
|
||||
restart: unless-stopped""",
|
||||
"required_variables": ["REPO_PATH", "TOOL_NAME"],
|
||||
},
|
||||
{
|
||||
"name": "opencode",
|
||||
"display_name": "OpenCode",
|
||||
"description": "AI coding assistant - run opencode in terminal",
|
||||
"category": "ai-assistant",
|
||||
"interfaces": ["terminal"],
|
||||
"default_port": 3000,
|
||||
"compose_template": """version: "3.8"
|
||||
services:
|
||||
opencode:
|
||||
image: node:20-slim
|
||||
container_name: {{TOOL_NAME}}
|
||||
working_dir: /workspace
|
||||
environment:
|
||||
- HOME=/tmp
|
||||
volumes:
|
||||
- {{REPO_PATH}}:/workspace
|
||||
- opencode_home:/tmp
|
||||
ports:
|
||||
- "3000:3000"
|
||||
command: >
|
||||
sh -c "set -x &&
|
||||
apt-get update && apt-get install -y git ca-certificates &&
|
||||
echo 'Installing opencode...' &&
|
||||
npm install -g opencode-ai 2>&1 || echo 'ERROR: npm install failed' &&
|
||||
which opencode || echo 'ERROR: opencode not in PATH' &&
|
||||
npm bin -g &&
|
||||
ls -la $(npm bin -g) || echo 'ERROR: global bin dir not found' &&
|
||||
echo 'export PATH=\"$(npm bin -g):\$PATH\"' >> /root/.bashrc &&
|
||||
echo 'cd /workspace' >> /root/.bashrc &&
|
||||
echo 'OpenCode installation complete' &&
|
||||
cd /workspace &&
|
||||
exec tail -f /dev/null"
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
opencode_home:""",
|
||||
"required_variables": ["REPO_PATH", "TOOL_NAME"],
|
||||
},
|
||||
]
|
||||
|
||||
for tool_data in builtin_types:
|
||||
existing = await session.scalar(select(ToolType).where(ToolType.name == tool_data["name"]))
|
||||
if not existing:
|
||||
tool_type = ToolType(
|
||||
name=tool_data["name"],
|
||||
display_name=tool_data["display_name"],
|
||||
description=tool_data["description"],
|
||||
category=tool_data["category"],
|
||||
interfaces=tool_data["interfaces"],
|
||||
definition_type="compose",
|
||||
compose_template=tool_data["compose_template"],
|
||||
required_variables=tool_data["required_variables"],
|
||||
default_port=tool_data.get("default_port"),
|
||||
is_builtin=True,
|
||||
)
|
||||
session.add(tool_type)
|
||||
logger.info("Created built-in tool type: %s", tool_data["name"])
|
||||
else:
|
||||
# Update existing built-in tool types to reflect code changes
|
||||
existing.display_name = tool_data["display_name"]
|
||||
existing.description = tool_data["description"]
|
||||
existing.category = tool_data["category"]
|
||||
existing.interfaces = tool_data["interfaces"]
|
||||
existing.definition_type = "compose"
|
||||
existing.compose_template = tool_data["compose_template"]
|
||||
existing.required_variables = tool_data["required_variables"]
|
||||
existing.default_port = tool_data.get("default_port")
|
||||
logger.info("Updated built-in tool type: %s", tool_data["name"])
|
||||
|
||||
await session.commit()
|
||||
logger.info("Built-in tool types seeded successfully.")
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def on_startup():
|
||||
logger.info("Starting up Headquarter API...")
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import logging
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from src.database import SessionLocal
|
||||
from src.models.tool_type import ToolType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _table_exists(session, table_name: str) -> bool:
|
||||
"""Check if a table exists in the database."""
|
||||
from sqlalchemy import text
|
||||
|
||||
try:
|
||||
result = await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = :table_name
|
||||
)
|
||||
"""
|
||||
),
|
||||
{"table_name": table_name},
|
||||
)
|
||||
return result.scalar() or False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
async def seed_builtin_tool_types():
|
||||
async with SessionLocal() as session:
|
||||
# Check if tool_types table exists before attempting to seed
|
||||
if not await _table_exists(session, "tool_types"):
|
||||
logger.warning(
|
||||
"tool_types table does not exist. Skipping seeding. "
|
||||
"Migrations may not have run yet."
|
||||
)
|
||||
return
|
||||
|
||||
builtin_types = [
|
||||
{
|
||||
"name": "code-server",
|
||||
"display_name": "VS Code Server",
|
||||
"description": "VS Code running in the browser via code-server",
|
||||
"category": "editor",
|
||||
"interfaces": ["web"],
|
||||
"compose_template": """version: "3.8"
|
||||
services:
|
||||
code-server:
|
||||
image: lscr.io/linuxserver/code-server:latest
|
||||
container_name: {{TOOL_NAME}}
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/London
|
||||
volumes:
|
||||
- {{REPO_PATH}}:/config/workspace
|
||||
ports:
|
||||
- "8443:8443"
|
||||
restart: unless-stopped""",
|
||||
"default_port": 8443,
|
||||
"required_variables": ["REPO_PATH", "TOOL_NAME"],
|
||||
},
|
||||
{
|
||||
"name": "jupyter-notebook",
|
||||
"display_name": "Jupyter Notebook",
|
||||
"description": "Jupyter Lab for interactive development",
|
||||
"category": "notebook",
|
||||
"interfaces": ["web"],
|
||||
"default_port": 8888,
|
||||
"compose_template": """version: "3.8"
|
||||
services:
|
||||
jupyter:
|
||||
image: jupyter/scipy-notebook:latest
|
||||
container_name: {{TOOL_NAME}}
|
||||
environment:
|
||||
- JUPYTER_ENABLE_LAB=yes
|
||||
volumes:
|
||||
- {{REPO_PATH}}:/home/jovyan/work
|
||||
ports:
|
||||
- "8888:8888"
|
||||
restart: unless-stopped""",
|
||||
"required_variables": ["REPO_PATH", "TOOL_NAME"],
|
||||
},
|
||||
{
|
||||
"name": "opencode",
|
||||
"display_name": "OpenCode",
|
||||
"description": "AI coding assistant - run opencode in terminal",
|
||||
"category": "ai-assistant",
|
||||
"interfaces": ["terminal"],
|
||||
"default_port": 3000,
|
||||
"compose_template": """version: "3.8"
|
||||
services:
|
||||
opencode:
|
||||
image: node:20-slim
|
||||
container_name: {{TOOL_NAME}}
|
||||
working_dir: /workspace
|
||||
environment:
|
||||
- HOME=/tmp
|
||||
volumes:
|
||||
- {{REPO_PATH}}:/workspace
|
||||
- opencode_home:/tmp
|
||||
ports:
|
||||
- "3000:3000"
|
||||
command: >
|
||||
sh -c "set -x &&
|
||||
apt-get update && apt-get install -y git ca-certificates &&
|
||||
echo 'Installing opencode...' &&
|
||||
npm install -g opencode-ai 2>&1 || echo 'ERROR: npm install failed' &&
|
||||
which opencode || echo 'ERROR: opencode not in PATH' &&
|
||||
npm bin -g &&
|
||||
ls -la $(npm bin -g) || echo 'ERROR: global bin dir not found' &&
|
||||
echo 'export PATH=\"$(npm bin -g):\\$PATH\"' >> /root/.bashrc &&
|
||||
echo 'cd /workspace' >> /root/.bashrc &&
|
||||
echo 'OpenCode installation complete' &&
|
||||
cd /workspace &&
|
||||
exec tail -f /dev/null"
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
opencode_home:""",
|
||||
"required_variables": ["REPO_PATH", "TOOL_NAME"],
|
||||
},
|
||||
]
|
||||
|
||||
for tool_data in builtin_types:
|
||||
existing = await session.scalar(select(ToolType).where(ToolType.name == tool_data["name"]))
|
||||
if not existing:
|
||||
tool_type = ToolType(
|
||||
name=tool_data["name"],
|
||||
display_name=tool_data["display_name"],
|
||||
description=tool_data["description"],
|
||||
category=tool_data["category"],
|
||||
interfaces=tool_data["interfaces"],
|
||||
definition_type="compose",
|
||||
compose_template=tool_data["compose_template"],
|
||||
required_variables=tool_data["required_variables"],
|
||||
default_port=tool_data.get("default_port"),
|
||||
is_builtin=True,
|
||||
)
|
||||
session.add(tool_type)
|
||||
logger.info("Created built-in tool type: %s", tool_data["name"])
|
||||
else:
|
||||
# Update existing built-in tool types to reflect code changes
|
||||
existing.display_name = tool_data["display_name"]
|
||||
existing.description = tool_data["description"]
|
||||
existing.category = tool_data["category"]
|
||||
existing.interfaces = tool_data["interfaces"]
|
||||
existing.definition_type = "compose"
|
||||
existing.compose_template = tool_data["compose_template"]
|
||||
existing.required_variables = tool_data["required_variables"]
|
||||
existing.default_port = tool_data.get("default_port")
|
||||
logger.info("Updated built-in tool type: %s", tool_data["name"])
|
||||
|
||||
await session.commit()
|
||||
logger.info("Built-in tool types seeded successfully.")
|
||||
@@ -1,38 +1,17 @@
|
||||
import { apiClient } from "./client";
|
||||
import type {
|
||||
ConfigFolder,
|
||||
CreateConfigFolderRequest,
|
||||
UpdateConfigFolderRequest,
|
||||
ProjectOverrideRequest,
|
||||
} from "../types/config-folder";
|
||||
|
||||
export interface ConfigFolder {
|
||||
id: string;
|
||||
user_id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
mount_path: string;
|
||||
files: Record<string, string>;
|
||||
project_overrides: Record<string, { mount_path?: string; files?: Record<string, string> }> | null;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateConfigFolderRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
mount_path: string;
|
||||
files?: Record<string, string>;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateConfigFolderRequest {
|
||||
name?: string;
|
||||
description?: string;
|
||||
mount_path?: string;
|
||||
files?: Record<string, string>;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export interface ProjectOverrideRequest {
|
||||
mount_path?: string;
|
||||
files?: Record<string, string>;
|
||||
}
|
||||
export type {
|
||||
ConfigFolder,
|
||||
CreateConfigFolderRequest,
|
||||
UpdateConfigFolderRequest,
|
||||
ProjectOverrideRequest,
|
||||
} from "../types/config-folder";
|
||||
|
||||
export const listConfigFolders = async (): Promise<ConfigFolder[]> => {
|
||||
const response = await apiClient.get<ConfigFolder[]>("/config-folders");
|
||||
@@ -55,7 +34,10 @@ export const updateConfigFolder = async (
|
||||
id: string,
|
||||
data: UpdateConfigFolderRequest
|
||||
): Promise<ConfigFolder> => {
|
||||
const response = await apiClient.put<ConfigFolder>(`/config-folders/${id}`, data);
|
||||
const response = await apiClient.put<ConfigFolder>(
|
||||
`/config-folders/${id}`,
|
||||
data
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,32 +1,26 @@
|
||||
import { apiClient } from "./client";
|
||||
import type {
|
||||
CommitDetail,
|
||||
CommitHistoryResponse,
|
||||
CommitResponse,
|
||||
GitRepository,
|
||||
GitRepositoryCreate,
|
||||
GitStatus,
|
||||
MergeResponse,
|
||||
URLParseResult,
|
||||
} from "../types/git-repository";
|
||||
|
||||
export interface GitRepository {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
project_id: string;
|
||||
owner_id: string;
|
||||
is_mirror: boolean;
|
||||
remote_url: string | null;
|
||||
last_push: string | null;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
export interface GitRepositoryCreate {
|
||||
name: string;
|
||||
remote_url?: string;
|
||||
force_original_url?: boolean;
|
||||
}
|
||||
|
||||
export interface URLParseResult {
|
||||
original_url: string;
|
||||
base_url: string | null;
|
||||
is_valid_clone_url: boolean;
|
||||
needs_parsing: boolean;
|
||||
host: string | null;
|
||||
message: string;
|
||||
error_code: string | null;
|
||||
}
|
||||
export type {
|
||||
CommitDetail,
|
||||
CommitHistoryEntry,
|
||||
CommitHistoryResponse,
|
||||
CommitResponse,
|
||||
GitRepository,
|
||||
GitRepositoryCreate,
|
||||
GitStatus,
|
||||
MergeResponse,
|
||||
URLParseResult,
|
||||
} from "../types/git-repository";
|
||||
|
||||
export async function parseGitUrl(url: string): Promise<URLParseResult> {
|
||||
const response = await apiClient.post("/projects/repositories/parse-url", { url });
|
||||
@@ -50,24 +44,6 @@ export async function deleteRepository(projectId: string, repoId: string): Promi
|
||||
await apiClient.delete(`/projects/${projectId}/repositories/${repoId}`);
|
||||
}
|
||||
|
||||
export interface CommitHistoryEntry {
|
||||
hash: string;
|
||||
short_hash: string;
|
||||
message: string;
|
||||
author_name: string;
|
||||
author_email: string;
|
||||
author_date: string;
|
||||
refs: string[];
|
||||
graph_symbol: string;
|
||||
graph_depth: number;
|
||||
}
|
||||
|
||||
export interface CommitHistoryResponse {
|
||||
commits: CommitHistoryEntry[];
|
||||
branches: string[];
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export async function getRepositoryHistory(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
@@ -83,25 +59,6 @@ export async function getRepositoryHistory(
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export interface CommitDetail {
|
||||
hash: string;
|
||||
short_hash: string;
|
||||
message: string;
|
||||
author_name: string;
|
||||
author_email: string;
|
||||
author_date: string;
|
||||
committer_name: string;
|
||||
committer_email: string;
|
||||
committer_date: string;
|
||||
stats: {
|
||||
additions: number;
|
||||
deletions: number;
|
||||
files_changed: number;
|
||||
};
|
||||
diff: string;
|
||||
parents: string[];
|
||||
}
|
||||
|
||||
export async function getCommitDetail(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
@@ -113,19 +70,6 @@ export async function getCommitDetail(
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Git Control API
|
||||
|
||||
export interface GitStatus {
|
||||
branch: string;
|
||||
modified: string[];
|
||||
added: string[];
|
||||
deleted: string[];
|
||||
untracked: string[];
|
||||
renamed: string[];
|
||||
ahead: number;
|
||||
behind: number;
|
||||
}
|
||||
|
||||
export async function getRepositoryStatus(
|
||||
projectId: string,
|
||||
repoId: string
|
||||
@@ -173,11 +117,6 @@ export async function checkoutBranch(
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export interface CommitResponse {
|
||||
commit_hash: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export async function commitChanges(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
@@ -225,11 +164,6 @@ export async function pushRepository(
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export interface MergeResponse {
|
||||
commit_hash: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export async function mergeBranches(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
|
||||
@@ -1,31 +1,9 @@
|
||||
import { apiClient } from "./client";
|
||||
import type { Session } from "../types/session";
|
||||
import type { ToolInstance } from "../types/tool-instance";
|
||||
|
||||
export interface ToolInstance {
|
||||
id: string;
|
||||
name: string;
|
||||
display_name: string;
|
||||
tool_type_id: string;
|
||||
tool_type_name: string;
|
||||
tool_type_interfaces: string[];
|
||||
status: string;
|
||||
url: string | null;
|
||||
port: number | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
id: string;
|
||||
display_name: string;
|
||||
tool_type_name: string;
|
||||
tool_icon: string;
|
||||
tool_type_interfaces: string[];
|
||||
repository_name: string;
|
||||
repository_id: string;
|
||||
project_name: string;
|
||||
project_id: string;
|
||||
status: string;
|
||||
url: string | null;
|
||||
}
|
||||
export type { Session } from "../types/session";
|
||||
export type { ToolInstance } from "../types/tool-instance";
|
||||
|
||||
export async function listInstances(
|
||||
projectId: string,
|
||||
|
||||
@@ -1,33 +1,7 @@
|
||||
import { apiClient } from "./client";
|
||||
import type { ToolConfig, CreateToolConfigRequest } from "../types/tool-config";
|
||||
|
||||
export interface ToolConfig {
|
||||
id: string;
|
||||
tool_type_id: string;
|
||||
project_id: string | null;
|
||||
key: string;
|
||||
value: string;
|
||||
config_type: string;
|
||||
file_path: string | null;
|
||||
port_override: number | null;
|
||||
start_command: string | null;
|
||||
working_directory: string | null;
|
||||
environment_variables: Record<string, string> | null;
|
||||
volumes: Array<{ source: string; target: string; type?: string }> | null;
|
||||
}
|
||||
|
||||
export interface CreateToolConfigRequest {
|
||||
tool_type_id: string;
|
||||
project_id?: string;
|
||||
key: string;
|
||||
value: string;
|
||||
config_type?: string;
|
||||
file_path?: string;
|
||||
port_override?: number;
|
||||
start_command?: string;
|
||||
working_directory?: string;
|
||||
environment_variables?: Record<string, string>;
|
||||
volumes?: Array<{ source: string; target: string; type?: string }>;
|
||||
}
|
||||
export type { ToolConfig, CreateToolConfigRequest } from "../types/tool-config";
|
||||
|
||||
export const listToolConfigs = async (
|
||||
tool_type_id?: string,
|
||||
@@ -36,7 +10,7 @@ export const listToolConfigs = async (
|
||||
const params = new URLSearchParams();
|
||||
if (tool_type_id) params.append("tool_type_id", tool_type_id);
|
||||
if (project_id) params.append("project_id", project_id);
|
||||
|
||||
|
||||
const response = await apiClient.get<{ configs: ToolConfig[] }>(
|
||||
`/tool-configs?${params.toString()}`
|
||||
);
|
||||
@@ -72,4 +46,4 @@ export const getToolConfigDefaults = async (
|
||||
`/tool-configs/defaults/${toolTypeId}`
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,59 +1,7 @@
|
||||
import { apiClient } from "./client";
|
||||
import type { ToolType, CreateToolTypeRequest, UpdateToolTypeRequest } from "../types/tool-type";
|
||||
|
||||
export interface ReadinessProbe {
|
||||
command: string;
|
||||
timeout: number;
|
||||
interval: number;
|
||||
}
|
||||
|
||||
export interface ToolType {
|
||||
id: string;
|
||||
name: string;
|
||||
display_name: string;
|
||||
description: string | null;
|
||||
category: string;
|
||||
interfaces: string[];
|
||||
default_port: number | null;
|
||||
definition_type: 'compose' | 'dockerfile';
|
||||
compose_template: string | null;
|
||||
dockerfile_template: string | null;
|
||||
build_context: Record<string, string> | null;
|
||||
readiness_probe: ReadinessProbe | null;
|
||||
required_variables: string[];
|
||||
is_builtin: boolean;
|
||||
created_by_id: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateToolTypeRequest {
|
||||
name: string;
|
||||
display_name: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
interfaces?: string[];
|
||||
default_port: number;
|
||||
definition_type?: 'compose' | 'dockerfile';
|
||||
compose_template?: string;
|
||||
dockerfile_template?: string;
|
||||
build_context?: Record<string, string>;
|
||||
readiness_probe?: ReadinessProbe;
|
||||
required_variables: string[];
|
||||
}
|
||||
|
||||
export interface UpdateToolTypeRequest {
|
||||
display_name?: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
interfaces?: string[];
|
||||
default_port?: number;
|
||||
definition_type?: 'compose' | 'dockerfile';
|
||||
compose_template?: string;
|
||||
dockerfile_template?: string;
|
||||
build_context?: Record<string, string>;
|
||||
readiness_probe?: ReadinessProbe;
|
||||
required_variables?: string[];
|
||||
}
|
||||
export type { ReadinessProbe, ToolType, CreateToolTypeRequest, UpdateToolTypeRequest } from "../types/tool-type";
|
||||
|
||||
export const listToolTypes = async (): Promise<ToolType[]> => {
|
||||
const response = await apiClient.get<ToolType[]>("/tool-types");
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect } from "react";
|
||||
import { Link, NavLink, Outlet } from "react-router-dom";
|
||||
|
||||
import { getUserSessions } from "../api/sessions";
|
||||
import type { Session } from "../api/sessions";
|
||||
import type { Session } from "../types/session";
|
||||
import { useTheme } from "../hooks/use-theme";
|
||||
import { useAuth } from "../state/auth";
|
||||
import { useSessions } from "../state/sessions";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Icon } from "./icon";
|
||||
import type { ToolInstance } from "../api/sessions";
|
||||
import type { ToolInstance } from "../types/tool-instance";
|
||||
import type { ToolType } from "../types/tool-type";
|
||||
import {
|
||||
checkInstanceHealth,
|
||||
createInstance,
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
startInstance,
|
||||
stopInstance,
|
||||
} from "../api/sessions";
|
||||
import type { ToolType } from "../api/tool_types";
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import { deleteRepository, listRepositories, type GitRepository } from "../api/git_repositories";
|
||||
import type { GitRepository } from "../types/git-repository";
|
||||
import { deleteRepository, listRepositories } from "../api/git_repositories";
|
||||
import { RepositoryCreateDialog } from "./repository-create-dialog";
|
||||
import { Icon } from "./icon";
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { createRepository, parseGitUrl, type GitRepositoryCreate, type URLParseResult } from "../api/git_repositories";
|
||||
import type { GitRepositoryCreate, URLParseResult } from "../types/git-repository";
|
||||
import { createRepository, parseGitUrl } from "../api/git_repositories";
|
||||
import { Icon } from "./icon";
|
||||
|
||||
type CreateMode = "clone" | "blank";
|
||||
|
||||
@@ -2,12 +2,15 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { getDashboardSummary, type DashboardSummary } from "../api/dashboard";
|
||||
import { createInstance, getUserSessions, startInstance, stopInstance, deleteInstance, recreateInstanceTunnel, type Session as SessionApi } from "../api/sessions";
|
||||
import { createInstance, getUserSessions, startInstance, stopInstance, deleteInstance, recreateInstanceTunnel } from "../api/sessions";
|
||||
import { listProjects } from "../api/projects";
|
||||
import { listRepositories, type GitRepository } from "../api/git_repositories";
|
||||
import { listToolTypes, type ToolType } from "../api/tool_types";
|
||||
import { listRepositories } from "../api/git_repositories";
|
||||
import { listToolTypes } from "../api/tool_types";
|
||||
import { updateUserConfig } from "../api/settings";
|
||||
import type { Project } from "../types";
|
||||
import type { Project } from "../types/project";
|
||||
import type { Session as SessionApi } from "../types/session";
|
||||
import type { GitRepository } from "../types/git-repository";
|
||||
import type { ToolType } from "../types/tool-type";
|
||||
import { Icon } from "../components/icon";
|
||||
|
||||
type HomeStatus = "loading" | "ready" | "error";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import type { GitRepository } from "../types/git-repository";
|
||||
import {
|
||||
deleteRepository,
|
||||
listRepositories,
|
||||
} from "../api/git_repositories";
|
||||
import type { GitRepository } from "../api/git_repositories";
|
||||
import { Icon } from "../components/icon";
|
||||
import { RepositoryCreateDialog } from "../components/repository-create-dialog";
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@ import { Icon } from "../components/icon";
|
||||
import { Link, useParams, useSearchParams } from "react-router-dom";
|
||||
|
||||
import { apiClient } from "../api/client";
|
||||
import type { GitRepository } from "../types/git-repository";
|
||||
import type { ToolType } from "../types/tool-type";
|
||||
import {
|
||||
getRepositoryStatus,
|
||||
listRepositories,
|
||||
type GitRepository,
|
||||
type GitStatus,
|
||||
} from "../api/git_repositories";
|
||||
import { CommitPanel } from "../components/commit-panel";
|
||||
@@ -16,7 +17,6 @@ import { GitToolbar } from "../components/git-toolbar";
|
||||
import { InstanceList } from "../components/instance-list";
|
||||
import { WorkspaceHeader } from "../components/workspace-header";
|
||||
import { listToolTypes } from "../api/tool_types";
|
||||
import type { ToolType } from "../api/tool_types";
|
||||
|
||||
type WorkspaceStatus = "loading" | "ready" | "error" | "empty";
|
||||
|
||||
|
||||
@@ -2,20 +2,22 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { listProjects } from "../api/projects";
|
||||
import type { Project } from "../types";
|
||||
import { listRepositories, type GitRepository } from "../api/git_repositories";
|
||||
import { listRepositories } from "../api/git_repositories";
|
||||
import {
|
||||
getUserSessions,
|
||||
type Session,
|
||||
deleteInstance,
|
||||
stopInstance,
|
||||
startInstance,
|
||||
checkInstanceHealth,
|
||||
recreateInstanceTunnel,
|
||||
createInstance,
|
||||
} from "../api/sessions";
|
||||
import { listToolTypes, type ToolType } from "../api/tool_types";
|
||||
import { createInstance } from "../api/sessions";
|
||||
import { listToolTypes } from "../api/tool_types";
|
||||
import { getUserConfig, updateUserConfig } from "../api/settings";
|
||||
import type { Project } from "../types/project";
|
||||
import type { Session } from "../types/session";
|
||||
import type { GitRepository } from "../types/git-repository";
|
||||
import type { ToolType } from "../types/tool-type";
|
||||
import { Icon } from "../components/icon";
|
||||
|
||||
type SessionsStatus = "loading" | "ready" | "error";
|
||||
|
||||
@@ -2,13 +2,14 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Icon } from "../components/icon";
|
||||
import { listToolTypes, type ToolType } from "../api/tool_types";
|
||||
import type { ToolType } from "../types/tool-type";
|
||||
import type { ToolConfig } from "../types/tool-config";
|
||||
import { listToolTypes } from "../api/tool_types";
|
||||
import {
|
||||
createToolConfig,
|
||||
deleteToolConfig,
|
||||
listToolConfigs,
|
||||
updateToolConfig,
|
||||
type ToolConfig,
|
||||
} from "../api/tool_configs";
|
||||
|
||||
type ConfigStatus = "loading" | "ready" | "error";
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import type { ToolType, CreateToolTypeRequest, UpdateToolTypeRequest } from "../types/tool-type";
|
||||
import {
|
||||
createToolType,
|
||||
deleteToolType,
|
||||
listToolTypes,
|
||||
updateToolType,
|
||||
type CreateToolTypeRequest,
|
||||
type UpdateToolTypeRequest,
|
||||
} from "../api/tool_types";
|
||||
import { Icon } from "../components/icon";
|
||||
import type { ToolType } from "../api/tool_types";
|
||||
|
||||
type ToolTypesStatus = "loading" | "ready" | "error";
|
||||
type DialogMode = "none" | "create" | "edit";
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
import { createContext, useCallback, useContext, useState, type ReactNode } from "react";
|
||||
import type { Session } from "../types/session";
|
||||
|
||||
export interface Session {
|
||||
id: string;
|
||||
display_name: string;
|
||||
tool_type_name: string;
|
||||
tool_icon: string;
|
||||
tool_type_interfaces: string[];
|
||||
repository_name: string;
|
||||
repository_id: string;
|
||||
project_name: string;
|
||||
project_id: string;
|
||||
status: string;
|
||||
url: string | null;
|
||||
}
|
||||
export type { Session } from "../types/session";
|
||||
|
||||
interface SessionsContextType {
|
||||
sessions: Session[];
|
||||
|
||||
+4
-17
@@ -1,18 +1,5 @@
|
||||
export type SessionUser = {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
avatar_url: string | null;
|
||||
};
|
||||
// Legacy types file — being migrated to types/ directory.
|
||||
// This file will be removed once all consumers are updated.
|
||||
|
||||
export type SessionPayload = {
|
||||
user: SessionUser;
|
||||
};
|
||||
|
||||
export type Project = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
owner_id: string;
|
||||
default_ssh_key_id: string | null;
|
||||
};
|
||||
export type { Project } from "./types/project";
|
||||
export type { SessionPayload, SessionUser } from "./types/user";
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface ApiResponse<T> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
export interface ConfigFolder {
|
||||
id: string;
|
||||
user_id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
mount_path: string;
|
||||
files: Record<string, string>;
|
||||
project_overrides: Record<string, { mount_path?: string; files?: Record<string, string> }> | null;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateConfigFolderRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
mount_path: string;
|
||||
files?: Record<string, string>;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateConfigFolderRequest {
|
||||
name?: string;
|
||||
description?: string;
|
||||
mount_path?: string;
|
||||
files?: Record<string, string>;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export interface ProjectOverrideRequest {
|
||||
mount_path?: string;
|
||||
files?: Record<string, string>;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
export interface GitRepository {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
project_id: string;
|
||||
owner_id: string;
|
||||
is_mirror: boolean;
|
||||
remote_url: string | null;
|
||||
last_push: string | null;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
export interface GitRepositoryCreate {
|
||||
name: string;
|
||||
remote_url?: string;
|
||||
force_original_url?: boolean;
|
||||
}
|
||||
|
||||
export interface URLParseResult {
|
||||
original_url: string;
|
||||
base_url: string | null;
|
||||
is_valid_clone_url: boolean;
|
||||
needs_parsing: boolean;
|
||||
host: string | null;
|
||||
message: string;
|
||||
error_code: string | null;
|
||||
}
|
||||
|
||||
export interface CommitHistoryEntry {
|
||||
hash: string;
|
||||
short_hash: string;
|
||||
message: string;
|
||||
author_name: string;
|
||||
author_email: string;
|
||||
author_date: string;
|
||||
refs: string[];
|
||||
graph_symbol: string;
|
||||
graph_depth: number;
|
||||
}
|
||||
|
||||
export interface CommitHistoryResponse {
|
||||
commits: CommitHistoryEntry[];
|
||||
branches: string[];
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface CommitDetail {
|
||||
hash: string;
|
||||
short_hash: string;
|
||||
message: string;
|
||||
author_name: string;
|
||||
author_email: string;
|
||||
author_date: string;
|
||||
committer_name: string;
|
||||
committer_email: string;
|
||||
committer_date: string;
|
||||
stats: {
|
||||
additions: number;
|
||||
deletions: number;
|
||||
files_changed: number;
|
||||
};
|
||||
diff: string;
|
||||
parents: string[];
|
||||
}
|
||||
|
||||
export interface GitStatus {
|
||||
branch: string;
|
||||
modified: string[];
|
||||
added: string[];
|
||||
deleted: string[];
|
||||
untracked: string[];
|
||||
renamed: string[];
|
||||
ahead: number;
|
||||
behind: number;
|
||||
}
|
||||
|
||||
export interface CommitResponse {
|
||||
commit_hash: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface MergeResponse {
|
||||
commit_hash: string;
|
||||
message: string;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export type { ApiResponse, PaginatedResponse } from "./api-response";
|
||||
export type { ConfigFolder, CreateConfigFolderRequest, UpdateConfigFolderRequest, ProjectOverrideRequest } from "./config-folder";
|
||||
export type {
|
||||
CommitDetail,
|
||||
CommitHistoryEntry,
|
||||
CommitHistoryResponse,
|
||||
CommitResponse,
|
||||
GitRepository,
|
||||
GitRepositoryCreate,
|
||||
GitStatus,
|
||||
MergeResponse,
|
||||
URLParseResult,
|
||||
} from "./git-repository";
|
||||
export type { Project } from "./project";
|
||||
export type { Session } from "./session";
|
||||
export type { ToolConfig, CreateToolConfigRequest } from "./tool-config";
|
||||
export type { ToolInstance } from "./tool-instance";
|
||||
export type {
|
||||
CreateToolTypeRequest,
|
||||
ReadinessProbe,
|
||||
ToolType,
|
||||
UpdateToolTypeRequest,
|
||||
} from "./tool-type";
|
||||
export type { SessionPayload, SessionUser } from "./user";
|
||||
@@ -0,0 +1,7 @@
|
||||
export type Project = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
owner_id: string;
|
||||
default_ssh_key_id: string | null;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
export interface Session {
|
||||
id: string;
|
||||
display_name: string;
|
||||
tool_type_name: string;
|
||||
tool_icon: string;
|
||||
tool_type_interfaces: string[];
|
||||
repository_name: string;
|
||||
repository_id: string;
|
||||
project_name: string;
|
||||
project_id: string;
|
||||
status: string;
|
||||
url: string | null;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export interface ToolConfig {
|
||||
id: string;
|
||||
tool_type_id: string;
|
||||
project_id: string | null;
|
||||
key: string;
|
||||
value: string;
|
||||
config_type: string;
|
||||
file_path: string | null;
|
||||
port_override: number | null;
|
||||
start_command: string | null;
|
||||
working_directory: string | null;
|
||||
environment_variables: Record<string, string> | null;
|
||||
volumes: Array<{ source: string; target: string; type?: string }> | null;
|
||||
}
|
||||
|
||||
export interface CreateToolConfigRequest {
|
||||
tool_type_id: string;
|
||||
project_id?: string;
|
||||
key: string;
|
||||
value: string;
|
||||
config_type?: string;
|
||||
file_path?: string;
|
||||
port_override?: number;
|
||||
start_command?: string;
|
||||
working_directory?: string;
|
||||
environment_variables?: Record<string, string>;
|
||||
volumes?: Array<{ source: string; target: string; type?: string }>;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export interface ToolInstance {
|
||||
id: string;
|
||||
name: string;
|
||||
display_name: string;
|
||||
tool_type_id: string;
|
||||
tool_type_name: string;
|
||||
tool_type_interfaces: string[];
|
||||
status: string;
|
||||
url: string | null;
|
||||
port: number | null;
|
||||
created_at: string;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
export interface ReadinessProbe {
|
||||
command: string;
|
||||
timeout: number;
|
||||
interval: number;
|
||||
}
|
||||
|
||||
export interface ToolType {
|
||||
id: string;
|
||||
name: string;
|
||||
display_name: string;
|
||||
description: string | null;
|
||||
category: string;
|
||||
interfaces: string[];
|
||||
default_port: number | null;
|
||||
definition_type: "compose" | "dockerfile";
|
||||
compose_template: string | null;
|
||||
dockerfile_template: string | null;
|
||||
build_context: Record<string, string> | null;
|
||||
readiness_probe: ReadinessProbe | null;
|
||||
required_variables: string[];
|
||||
is_builtin: boolean;
|
||||
created_by_id: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateToolTypeRequest {
|
||||
name: string;
|
||||
display_name: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
interfaces?: string[];
|
||||
default_port: number;
|
||||
definition_type?: "compose" | "dockerfile";
|
||||
compose_template?: string;
|
||||
dockerfile_template?: string;
|
||||
build_context?: Record<string, string>;
|
||||
readiness_probe?: ReadinessProbe;
|
||||
required_variables: string[];
|
||||
}
|
||||
|
||||
export interface UpdateToolTypeRequest {
|
||||
display_name?: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
interfaces?: string[];
|
||||
default_port?: number;
|
||||
definition_type?: "compose" | "dockerfile";
|
||||
compose_template?: string;
|
||||
dockerfile_template?: string;
|
||||
build_context?: Record<string, string>;
|
||||
readiness_probe?: ReadinessProbe;
|
||||
required_variables?: string[];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export type SessionUser = {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
avatar_url: string | null;
|
||||
};
|
||||
|
||||
export type SessionPayload = {
|
||||
user: SessionUser;
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
# Task 1.1 Apply Report: Centralize Types and Extract Seed Data
|
||||
|
||||
**Status:** Success
|
||||
|
||||
**Files Created (13):**
|
||||
- `apps/web/src/types/session.ts` — Canonical Session interface
|
||||
- `apps/web/src/types/tool-instance.ts` — Canonical ToolInstance interface
|
||||
- `apps/web/src/types/tool-type.ts` — ToolType + ReadinessProbe + request types
|
||||
- `apps/web/src/types/git-repository.ts` — GitRepository + related types (GitStatus, CommitDetail, etc.)
|
||||
- `apps/web/src/types/config-folder.ts` — ConfigFolder + request types
|
||||
- `apps/web/src/types/tool-config.ts` — ToolConfig + request types
|
||||
- `apps/web/src/types/project.ts` — Project type
|
||||
- `apps/web/src/types/user.ts` — SessionUser + SessionPayload
|
||||
- `apps/web/src/types/api-response.ts` — Generic ApiResponse<T> + PaginatedResponse<T>
|
||||
- `apps/web/src/types/index.ts` — Barrel export for all domain types
|
||||
- `apps/api/src/seeds/__init__.py` — Package marker
|
||||
- `apps/api/src/seeds/builtin_tool_types.py` — Extracted seed data + seed function
|
||||
|
||||
**Files Modified (17):**
|
||||
- `apps/web/src/api/sessions.ts` — Removed inline Session/ToolInstance, import + re-export from types/
|
||||
- `apps/web/src/api/tool_types.ts` — Removed inline ToolType/ReadinessProbe/requests, import + re-export from types/
|
||||
- `apps/web/src/api/git_repositories.ts` — Removed inline GitRepository + related types, import + re-export from types/
|
||||
- `apps/web/src/api/config_folders.ts` — Removed inline ConfigFolder + requests, import + re-export from types/
|
||||
- `apps/web/src/api/tool_configs.ts` — Removed inline ToolConfig + requests, import + re-export from types/
|
||||
- `apps/web/src/state/sessions.tsx` — Removed inline Session, imports from types/session.ts
|
||||
- `apps/web/src/types.ts` — Removed Project/User/SessionPayload (now re-export from types/)
|
||||
- `apps/web/src/components/app-shell.tsx` — Updated Session import to types/session.ts
|
||||
- `apps/web/src/components/instance-list.tsx` — Updated ToolInstance/ToolType imports to types/
|
||||
- `apps/web/src/components/repositories-settings-tab.tsx` — Updated GitRepository import to types/
|
||||
- `apps/web/src/components/repository-create-dialog.tsx` — Updated GitRepositoryCreate/URLParseResult imports to types/
|
||||
- `apps/web/src/pages/dashboard.tsx` — Updated SessionApi/GitRepository/ToolType/Project imports to types/
|
||||
- `apps/web/src/pages/sessions.tsx` — Updated Session/GitRepository/ToolType/Project imports to types/
|
||||
- `apps/web/src/pages/repo-workspace.tsx` — Updated GitRepository/ToolType imports to types/
|
||||
- `apps/web/src/pages/tool-types.tsx` — Updated ToolType/CreateToolTypeRequest/UpdateToolTypeRequest imports to types/
|
||||
- `apps/web/src/pages/tool-configs.tsx` — Updated ToolType/ToolConfig imports to types/
|
||||
- `apps/web/src/pages/git-repositories.tsx` — Updated GitRepository import to types/
|
||||
- `apps/api/src/main.py` — Removed inline seed_builtin_tool_types, imports from seeds.builtin_tool_types
|
||||
|
||||
**Files Deleted:** None
|
||||
|
||||
**Quality Gate Results:**
|
||||
- `npm run typecheck` (frontend): **PASS** — zero errors
|
||||
- `npm run lint` (frontend): **PASS** — zero warnings
|
||||
- Python syntax check (backend main.py + seeds): **PASS** — exit code 0
|
||||
- Type uniqueness verification:
|
||||
- `interface Session` appears exactly once (in types/session.ts)
|
||||
- `interface ToolInstance` appears exactly once (in types/tool-instance.ts)
|
||||
- `interface ToolType` appears exactly once (in types/tool-type.ts)
|
||||
- `interface GitRepository` appears exactly once (in types/git-repository.ts)
|
||||
|
||||
**Blockers/Deviations:**
|
||||
- None. All types successfully centralized with backward-compatible re-exports from API modules.
|
||||
- The `types.ts` file at `apps/web/src/types.ts` still exists as a legacy re-export file to avoid breaking any remaining consumers. It will be removed in a later phase once all imports are confirmed migrated.
|
||||
@@ -0,0 +1,723 @@
|
||||
# Design: Repository Restructuring and Modularization
|
||||
|
||||
## Overview
|
||||
|
||||
This design document defines the exact target file layout, import patterns, barrel export structure, and per-phase migration mechanics for the repo restructuring. Every old file is mapped to its new location. All decisions from the spec are implemented concretely.
|
||||
|
||||
**Key decisions:**
|
||||
- CSS Modules for component-scoped styles
|
||||
- Flat `api/` backend structure (no versioning yet)
|
||||
- Feature components at `components/features/{domain}/`
|
||||
- Barrel exports for `components/ui/`, `components/features/{domain}/`, `types/`
|
||||
- Merge each phase to `main` immediately
|
||||
|
||||
---
|
||||
|
||||
## 1. Target Directory Structure
|
||||
|
||||
### 1.1 Frontend (`apps/web/src/`)
|
||||
|
||||
```
|
||||
src/
|
||||
├── api/ # API clients — NO types, NO barrel exports
|
||||
│ ├── client.ts
|
||||
│ ├── config-folders.ts # renamed: config_folders.ts → kebab-case
|
||||
│ ├── config-profiles.ts
|
||||
│ ├── dashboard.ts
|
||||
│ ├── git-repositories.ts
|
||||
│ ├── profile.ts
|
||||
│ ├── projects.ts
|
||||
│ ├── sessions.ts
|
||||
│ ├── settings.ts
|
||||
│ ├── ssh-keys.ts
|
||||
│ ├── tool-configs.ts
|
||||
│ ├── tool-types.ts
|
||||
│ └── user-config.ts
|
||||
│
|
||||
├── components/
|
||||
│ ├── layout/ # App-level layout
|
||||
│ │ ├── AppShell.tsx # renamed: app-shell.tsx
|
||||
│ │ ├── AppShell.module.css
|
||||
│ │ ├── Navigation.tsx
|
||||
│ │ ├── Navigation.module.css
|
||||
│ │ ├── UserChip.tsx
|
||||
│ │ └── index.ts # barrel: export { AppShell, Navigation }
|
||||
│ │
|
||||
│ ├── ui/ # Primitive UI components
|
||||
│ │ ├── Button.tsx
|
||||
│ │ ├── Button.module.css
|
||||
│ │ ├── Card.tsx
|
||||
│ │ ├── Card.module.css
|
||||
│ │ ├── Dialog.tsx
|
||||
│ │ ├── Dialog.module.css
|
||||
│ │ ├── Input.tsx
|
||||
│ │ ├── Input.module.css
|
||||
│ │ ├── LoadingState.tsx
|
||||
│ │ ├── ErrorState.tsx
|
||||
│ │ ├── StatusBadge.tsx
|
||||
│ │ └── index.ts # barrel
|
||||
│ │
|
||||
│ └── features/ # Domain-specific components
|
||||
│ ├── git/
|
||||
│ │ ├── FileBrowser.tsx # extracted from repo-workspace.tsx
|
||||
│ │ ├── FileBrowser.module.css
|
||||
│ │ ├── GitToolbar.tsx # renamed: git-toolbar.tsx
|
||||
│ │ ├── GitToolbar.module.css
|
||||
│ │ ├── CommitPanel.tsx
|
||||
│ │ ├── CommitPanel.module.css
|
||||
│ │ ├── CommitDialog.tsx
|
||||
│ │ ├── CommitDialog.module.css
|
||||
│ │ ├── MergeDialog.tsx
|
||||
│ │ ├── MergeDialog.module.css
|
||||
│ │ ├── FileEditor.tsx # renamed: file-editor.tsx
|
||||
│ │ ├── FileEditor.module.css
|
||||
│ │ ├── SyntaxHighlighter.tsx
|
||||
│ │ └── index.ts # barrel
|
||||
│ │
|
||||
│ ├── project/
|
||||
│ │ ├── ProjectCard.tsx
|
||||
│ │ ├── ProjectCard.module.css
|
||||
│ │ ├── ProjectList.tsx
|
||||
│ │ ├── CreateProjectDialog.tsx
|
||||
│ │ ├── DeleteConfirmDialog.tsx
|
||||
│ │ ├── RepositoryCard.tsx
|
||||
│ │ ├── RepositoryCreateDialog.tsx
|
||||
│ │ ├── RepositoriesSettingsTab.tsx
|
||||
│ │ └── index.ts # barrel
|
||||
│ │
|
||||
│ ├── session/
|
||||
│ │ ├── InstanceList.tsx # renamed: instance-list.tsx
|
||||
│ │ ├── InstanceList.module.css
|
||||
│ │ ├── InstanceCard.tsx
|
||||
│ │ ├── CreateInstanceDialog.tsx
|
||||
│ │ ├── SessionCard.tsx
|
||||
│ │ ├── SessionList.tsx
|
||||
│ │ ├── CreateSessionForm.tsx
|
||||
│ │ └── index.ts # barrel
|
||||
│ │
|
||||
│ ├── settings/
|
||||
│ │ ├── SettingsTabLayout.tsx
|
||||
│ │ ├── GeneralSettingsTab.tsx
|
||||
│ │ └── index.ts # barrel
|
||||
│ │
|
||||
│ ├── terminal/
|
||||
│ │ ├── TerminalComponent.tsx # renamed: terminal.tsx
|
||||
│ │ ├── TerminalComponent.module.css
|
||||
│ │ └── index.ts
|
||||
│ │
|
||||
│ └── workspace/
|
||||
│ ├── WorkspaceHeader.tsx
|
||||
│ └── index.ts
|
||||
│
|
||||
├── hooks/
|
||||
│ ├── use-theme.ts
|
||||
│ ├── use-auth.ts # extracted from state/auth.tsx? No — keep in state/
|
||||
│ ├── use-api-query.ts # NEW: reusable data fetching
|
||||
│ ├── use-local-storage.ts # NEW
|
||||
│ └── use-debounce.ts # NEW: extracted from use-terminal-connection
|
||||
│
|
||||
├── pages/ # Route entry points ONLY
|
||||
│ ├── DashboardPage.tsx # renamed: dashboard.tsx
|
||||
│ ├── DashboardPage.module.css
|
||||
│ ├── GitHistoryPage.tsx # renamed: git-history.tsx
|
||||
│ ├── GitRepositoriesPage.tsx # renamed: git-repositories.tsx
|
||||
│ ├── ProfilePage.tsx # renamed: profile.tsx
|
||||
│ ├── ProjectSettingsPage.tsx # renamed: project-settings.tsx
|
||||
│ ├── ProjectsPage.tsx # renamed: projects.tsx
|
||||
│ ├── RepoWorkspacePage.tsx # renamed: repo-workspace.tsx
|
||||
│ ├── SessionsPage.tsx # renamed: sessions.tsx
|
||||
│ ├── SettingsPage.tsx # renamed: settings.tsx
|
||||
│ ├── SshKeysPage.tsx # renamed: ssh-keys.tsx
|
||||
│ ├── TerminalPage.tsx # renamed: terminal.tsx
|
||||
│ ├── ToolConfigsPage.tsx # renamed: tool-configs.tsx
|
||||
│ ├── ToolTypesPage.tsx # renamed: tool-types.tsx
|
||||
│ ├── ToolWorkshopPage.tsx # renamed: tool-workshop.tsx
|
||||
│ └── PlaceholderPage.tsx # renamed: placeholder.tsx
|
||||
│
|
||||
├── router.tsx # unchanged
|
||||
│
|
||||
├── state/
|
||||
│ ├── auth.tsx # keep — context is state layer
|
||||
│ └── sessions.tsx # keep — imports from types/session.ts
|
||||
│
|
||||
├── styles/
|
||||
│ ├── tokens.css # CSS variables / design tokens
|
||||
│ ├── global.css # reset, body, shell layout grid
|
||||
│ ├── utilities.css # .truncate, .stack, .row, etc.
|
||||
│ ├── pages/
|
||||
│ │ ├── sessions.css # page-specific layout only
|
||||
│ │ ├── repo-workspace.css
|
||||
│ │ └── tool-workshop.css
|
||||
│ └── syntax-highlight.css # Prism.js overrides
|
||||
│
|
||||
├── types/ # ALL domain types centralized
|
||||
│ ├── index.ts # barrel: re-exports all
|
||||
│ ├── api-response.ts # generic ApiResponse<T>, PaginatedResponse<T>
|
||||
│ ├── config-folder.ts
|
||||
│ ├── config-profile.ts
|
||||
│ ├── git-repository.ts
|
||||
│ ├── project.ts
|
||||
│ ├── session.ts # canonical Session definition
|
||||
│ ├── ssh-key.ts
|
||||
│ ├── terminal.ts # merged from types/terminal.ts
|
||||
│ ├── tool-config.ts
|
||||
│ ├── tool-instance.ts # canonical ToolInstance definition
|
||||
│ ├── tool-type.ts
|
||||
│ ├── user.ts
|
||||
│ └── user-config.ts
|
||||
│
|
||||
├── utils/
|
||||
│ ├── icons.ts
|
||||
│ ├── language.ts
|
||||
│ └── terminal-protocol.ts
|
||||
│
|
||||
├── main.tsx # import entry point for styles
|
||||
└── test/
|
||||
└── setup.ts
|
||||
```
|
||||
|
||||
### 1.2 Backend (`apps/api/src/`)
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.py # router mounting + middleware ONLY (target: <100 lines)
|
||||
├── config.py # unchanged
|
||||
├── database.py # unchanged
|
||||
├── logging_config.py # unchanged
|
||||
│
|
||||
├── auth/
|
||||
│ ├── __init__.py
|
||||
│ ├── cookies.py
|
||||
│ ├── dependencies.py # shared: get_current_user, get_owned_project
|
||||
│ ├── oidc.py
|
||||
│ └── session.py
|
||||
│
|
||||
├── api/ # flat — no v1/ yet
|
||||
│ ├── __init__.py
|
||||
│ ├── auth.py # ~200 lines (target)
|
||||
│ ├── config_folders.py # ~200 lines (target)
|
||||
│ ├── config_profiles.py # ~250 lines (target) — CRUD only
|
||||
│ ├── dashboard.py # ~65 lines (unchanged)
|
||||
│ ├── git_repositories.py # ~250 lines (target) — CRUD only
|
||||
│ ├── health.py # ~150 lines (unchanged)
|
||||
│ ├── instance_proxy.py # ~125 lines (unchanged)
|
||||
│ ├── projects.py # ~200 lines (target)
|
||||
│ ├── ssh_keys.py # ~170 lines (target)
|
||||
│ ├── terminal.py # ~158 lines (unchanged)
|
||||
│ ├── tool_configs.py # ~200 lines (target)
|
||||
│ ├── tool_instances.py # ~250 lines (target) — CRUD + lifecycle endpoints only
|
||||
│ ├── tool_types.py # ~250 lines (target)
|
||||
│ ├── user_config.py # ~121 lines (unchanged)
|
||||
│ └── users.py # ~156 lines (unchanged)
|
||||
│
|
||||
├── models/ # unchanged — already well-organized
|
||||
│
|
||||
├── schemas/ # NEW: Pydantic request/response schemas
|
||||
│ ├── __init__.py
|
||||
│ ├── config_folder.py
|
||||
│ ├── config_profile.py
|
||||
│ ├── git_repository.py
|
||||
│ ├── project.py
|
||||
│ ├── ssh_key.py
|
||||
│ ├── tool_config.py
|
||||
│ ├── tool_instance.py
|
||||
│ ├── tool_type.py
|
||||
│ ├── user.py
|
||||
│ └── user_config.py
|
||||
│
|
||||
├── services/
|
||||
│ ├── __init__.py
|
||||
│ ├── docker/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── compose.py # compose file generation (≤300 lines)
|
||||
│ │ ├── container.py # container lifecycle (≤300 lines)
|
||||
│ │ ├── tunnel.py # Cloudflare tunnel (≤200 lines)
|
||||
│ │ └── config_staging.py # config folder file writing (≤200 lines)
|
||||
│ ├── docker_build.py # unchanged (~69 lines)
|
||||
│ ├── git/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── control.py # renamed: git_control.py
|
||||
│ │ ├── files.py # renamed: git_files.py
|
||||
│ │ └── history.py # renamed: git_history.py
|
||||
│ ├── profile_resolver.py # unchanged (~251 lines)
|
||||
│ ├── readiness_probe.py # unchanged (~66 lines)
|
||||
│ ├── terminal_manager.py # unchanged (~193 lines)
|
||||
│ └── terminal_session.py # unchanged (~162 lines)
|
||||
│
|
||||
├── seeds/
|
||||
│ ├── __init__.py
|
||||
│ └── builtin_tool_types.py # extracted from main.py
|
||||
│
|
||||
├── utils/
|
||||
│ ├── git_url_parser.py # unchanged
|
||||
│ └── ... # keep existing
|
||||
│
|
||||
└── scripts/
|
||||
└── seed.py # unchanged
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Barrel Export Patterns
|
||||
|
||||
### 2.1 Frontend Barrels
|
||||
|
||||
**`components/ui/index.ts`:**
|
||||
```typescript
|
||||
export { Button } from "./Button";
|
||||
export { Card } from "./Card";
|
||||
export { Dialog } from "./Dialog";
|
||||
export { Input } from "./Input";
|
||||
export { LoadingState } from "./LoadingState";
|
||||
export { ErrorState } from "./ErrorState";
|
||||
export { StatusBadge } from "./StatusBadge";
|
||||
```
|
||||
|
||||
**`components/features/git/index.ts`:**
|
||||
```typescript
|
||||
export { FileBrowser } from "./FileBrowser";
|
||||
export { GitToolbar } from "./GitToolbar";
|
||||
export { CommitPanel } from "./CommitPanel";
|
||||
export { CommitDialog } from "./CommitDialog";
|
||||
export { MergeDialog } from "./MergeDialog";
|
||||
export { FileEditor } from "./FileEditor";
|
||||
export { SyntaxHighlighter } from "./SyntaxHighlighter";
|
||||
```
|
||||
|
||||
**`types/index.ts`:**
|
||||
```typescript
|
||||
export type { ApiResponse, PaginatedResponse } from "./api-response";
|
||||
export type { ConfigFolder } from "./config-folder";
|
||||
export type { ConfigProfile } from "./config-profile";
|
||||
export type { GitRepository } from "./git-repository";
|
||||
export type { Project } from "./project";
|
||||
export type { Session } from "./session";
|
||||
export type { SshKey } from "./ssh-key";
|
||||
export type { TerminalConnectionState, ClientControlMessage, ServerControlMessage } from "./terminal";
|
||||
export type { ToolConfig } from "./tool-config";
|
||||
export type { ToolInstance } from "./tool-instance";
|
||||
export type { ToolType } from "./tool-type";
|
||||
export type { User } from "./user";
|
||||
export type { UserConfig } from "./user-config";
|
||||
```
|
||||
|
||||
### 2.2 Backend Barrels
|
||||
|
||||
**`services/docker/__init__.py`:**
|
||||
```python
|
||||
from .compose import generate_compose, modify_compose
|
||||
from .container import create_container, start_container, stop_container, remove_container
|
||||
from .tunnel import create_tunnel, recreate_tunnel, check_tunnel_health
|
||||
from .config_staging import stage_config_files
|
||||
|
||||
__all__ = [
|
||||
"generate_compose", "modify_compose",
|
||||
"create_container", "start_container", "stop_container", "remove_container",
|
||||
"create_tunnel", "recreate_tunnel", "check_tunnel_health",
|
||||
"stage_config_files",
|
||||
]
|
||||
```
|
||||
|
||||
**`services/git/__init__.py`:**
|
||||
```python
|
||||
from .control import branch, checkout, commit, fetch, pull, push, merge
|
||||
from .files import list_files, read_file, write_file
|
||||
from .history import get_history, get_commit_detail, get_diff
|
||||
|
||||
__all__ = [
|
||||
"branch", "checkout", "commit", "fetch", "pull", "push", "merge",
|
||||
"list_files", "read_file", "write_file",
|
||||
"get_history", "get_commit_detail", "get_diff",
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Import Pattern Examples
|
||||
|
||||
### 3.1 Frontend Imports (After Refactor)
|
||||
|
||||
**Page component — orchestration only:**
|
||||
```typescript
|
||||
// pages/RepoWorkspacePage.tsx
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
import { WorkspaceHeader } from "@/components/features/workspace";
|
||||
import { FileBrowser, GitToolbar, CommitPanel } from "@/components/features/git";
|
||||
import { InstanceList } from "@/components/features/session";
|
||||
import { FileEditor } from "@/components/features/git";
|
||||
import { useApiQuery } from "@/hooks/use-api-query";
|
||||
import type { Project, GitRepository } from "@/types";
|
||||
```
|
||||
|
||||
**Feature component — self-contained:**
|
||||
```typescript
|
||||
// components/features/git/FileBrowser.tsx
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Icon } from "@/components/ui";
|
||||
import { apiClient } from "@/api/client";
|
||||
import type { FileTreeEntry, GitStatus } from "@/types";
|
||||
import styles from "./FileBrowser.module.css";
|
||||
```
|
||||
|
||||
**API module — pure functions, no types:**
|
||||
```typescript
|
||||
// api/git-repositories.ts
|
||||
import { apiClient } from "./client";
|
||||
import type { GitRepository, GitStatus, FileTreeEntry } from "@/types";
|
||||
|
||||
export async function listRepositories(projectId: string): Promise<GitRepository[]> { ... }
|
||||
```
|
||||
|
||||
### 3.2 Backend Imports (After Refactor)
|
||||
|
||||
**Router — thin, delegates to services:**
|
||||
```python
|
||||
# api/tool_instances.py
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from ..auth.dependencies import get_current_user, get_owned_project
|
||||
from ..database import get_db
|
||||
from ..models import User, Project
|
||||
from ..schemas.tool_instance import CreateInstanceRequest, InstanceResponse
|
||||
from ..services.docker import container, tunnel, compose
|
||||
from ..services.profile_resolver import resolve_profile
|
||||
|
||||
router = APIRouter(prefix="/projects/{project_id}/repositories/{repo_id}/instances")
|
||||
|
||||
@router.post("", response_model=InstanceResponse)
|
||||
async def create_instance(
|
||||
project_id: str,
|
||||
repo_id: str,
|
||||
request: CreateInstanceRequest,
|
||||
user: User = Depends(get_current_user),
|
||||
project: Project = Depends(get_owned_project),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
compose_content = compose.generate_compose(...)
|
||||
await container.create_container(...)
|
||||
return InstanceResponse(...)
|
||||
```
|
||||
|
||||
**Service — pure business logic:**
|
||||
```python
|
||||
# services/docker/container.py
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from .compose import generate_compose
|
||||
from .config_staging import stage_config_files
|
||||
|
||||
def create_container(instance_id: str, project_id: str, compose_path: Path) -> dict:
|
||||
stage_config_files(instance_id)
|
||||
result = subprocess.run(
|
||||
["docker", "compose", "-f", str(compose_path), "up", "-d"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. CSS Modules Migration Strategy
|
||||
|
||||
### 4.1 How It Works
|
||||
|
||||
Vite has built-in CSS Modules support. Naming a file `{name}.module.css` makes Vite:
|
||||
1. Scope all class names to that component
|
||||
2. Export a mapping object from the import
|
||||
|
||||
```typescript
|
||||
import styles from "./Button.module.css";
|
||||
|
||||
// In JSX:
|
||||
<button className={styles.primary}>Click</button>
|
||||
// → renders as: <button class="Button_primary__a3f7b">Click</button>
|
||||
```
|
||||
|
||||
### 4.2 Migration Mechanics
|
||||
|
||||
**Step 1: Extract component styles from `styles.css`**
|
||||
For each component, find its CSS rules in `styles.css` and move them to `{Component}.module.css`.
|
||||
|
||||
Example — `FileBrowser`:
|
||||
```css
|
||||
/* components/features/git/FileBrowser.module.css */
|
||||
.fileBrowser { padding: 0.5rem; overflow: auto; }
|
||||
.treeEntry { display: block; padding: 0.375rem 0.5rem; ... }
|
||||
.treeDirectory { font-weight: 500; }
|
||||
/* etc. */
|
||||
```
|
||||
|
||||
**Step 2: Convert global class names to camelCase in the module**
|
||||
Original: `.file-tree`, `.tree-entry`, `.tree-directory`
|
||||
Module: `.fileBrowser`, `.treeEntry`, `.treeDirectory`
|
||||
|
||||
**Step 3: Update component to import the module**
|
||||
```typescript
|
||||
import styles from "./FileBrowser.module.css";
|
||||
|
||||
// Before: <div className="file-tree">
|
||||
// After: <div className={styles.fileBrowser}>
|
||||
```
|
||||
|
||||
### 4.3 Global Styles That Stay Global
|
||||
|
||||
These rules remain in `styles/global.css` or `styles/utilities.css`:
|
||||
|
||||
```css
|
||||
/* styles/global.css */
|
||||
:root { /* CSS variables */ }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--bg); }
|
||||
|
||||
/* Shell layout — used by AppShell only */
|
||||
.shell { min-height: 100vh; display: flex; flex-direction: column; }
|
||||
.shell-body { display: grid; grid-template-columns: 230px 1fr; }
|
||||
```
|
||||
|
||||
```css
|
||||
/* styles/utilities.css */
|
||||
.stack { display: flex; flex-direction: column; gap: 1rem; }
|
||||
.row { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; }
|
||||
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
```
|
||||
|
||||
### 4.4 Page-Specific Layout Styles
|
||||
|
||||
Some pages need layout rules that don't belong to any single component:
|
||||
|
||||
```css
|
||||
/* styles/pages/repo-workspace.css */
|
||||
.repo-workspace { display: flex; flex-direction: column; height: calc(100vh - 60px); }
|
||||
.workspace-layout { display: flex; flex: 1; overflow: hidden; }
|
||||
.workspace-sidebar { width: 280px; min-width: 280px; ... }
|
||||
```
|
||||
|
||||
These are imported by the page component:
|
||||
```typescript
|
||||
import "@/styles/pages/repo-workspace.css";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Per-Phase Migration Mechanics
|
||||
|
||||
### Phase 1: Safe Foundations
|
||||
|
||||
**Goal:** Low-risk extractions that establish the new patterns without touching many files.
|
||||
|
||||
| Action | Old Location | New Location | Technique |
|
||||
|--------|-------------|--------------|-----------|
|
||||
| Extract `FileBrowser` | `pages/repo-workspace.tsx` (inline) | `components/features/git/FileBrowser.tsx` | Cut-paste + import rewrite |
|
||||
| Create `types/session.ts` | `api/sessions.ts` + `state/sessions.tsx` | `types/session.ts` | Extract shared interface |
|
||||
| Create `types/tool-instance.ts` | `api/sessions.ts` | `types/tool-instance.ts` | Extract interface |
|
||||
| Create `types/tool-type.ts` | `api/tool-types.ts` | `types/tool-type.ts` | Extract interface |
|
||||
| Create `types/git-repository.ts` | `api/git-repositories.ts` | `types/git-repository.ts` | Extract interface |
|
||||
| Create `types/project.ts` | `types.ts` + scattered | `types/project.ts` | Extract from types.ts |
|
||||
| Create `types/user.ts` | `types.ts` + `api/auth.ts` | `types/user.ts` | Extract from types.ts |
|
||||
| Create `types/api-response.ts` | Nowhere (new) | `types/api-response.ts` | New file for generic types |
|
||||
| Update `api/sessions.ts` | inline types | imports from `types/` | Import rewrite |
|
||||
| Update `state/sessions.tsx` | inline `Session` | imports from `types/session.ts` | Import rewrite |
|
||||
| Move seed data | `main.py` (hardcoded) | `seeds/builtin_tool_types.py` | Cut-paste + import |
|
||||
| Extract auth deps | Duplicated in routers | `auth/dependencies.py` + `api/` imports | Cut-paste + import rewrite |
|
||||
| Create barrel | `types/` | `types/index.ts` | New file |
|
||||
|
||||
**Quality gate:** `tsc --noEmit`, `pytest`, verify `repo-workspace.tsx` still works
|
||||
|
||||
### Phase 2: Style System
|
||||
|
||||
**Goal:** Replace `styles.css` with modular styles. This is the largest diff but lowest risk (no JS logic changes).
|
||||
|
||||
| Action | Old | New | Technique |
|
||||
|--------|-----|-----|-----------|
|
||||
| Create `styles/tokens.css` | `styles.css` (variables section) | New file | Extract `:root` and `[data-theme="dark"]` |
|
||||
| Create `styles/global.css` | `styles.css` (reset + layout) | New file | Extract `*`, `body`, `.shell-*` |
|
||||
| Create `styles/utilities.css` | `styles.css` (utility classes) | New file | Extract `.stack`, `.row`, `.truncate`, etc. |
|
||||
| Create `styles/syntax-highlight.css` | `styles.css` (Prism overrides) | New file | Extract all `code[class*="language-"]` rules |
|
||||
| Create component `.module.css` files | `styles.css` (component sections) | Per-component files | Extract `.terminal-*`, `.git-toolbar`, `.file-editor`, etc. |
|
||||
| Create page layout CSS files | `styles.css` (page sections) | `styles/pages/*.css` | Extract `.repo-workspace`, `.sessions-page`, etc. |
|
||||
| Delete `styles.css` | `styles.css` | — | `git rm` |
|
||||
| Update `main.tsx` | imports `styles.css` | imports `styles/global.css`, `styles/tokens.css`, etc. | Edit import |
|
||||
| Update components | use global class names | import `.module.css` and use `styles.className` | Edit JSX + add CSS file |
|
||||
|
||||
**Migration order within Phase 2:**
|
||||
1. Extract tokens + global + utilities + syntax-highlight (safe, no component changes)
|
||||
2. Extract component styles one domain at a time: terminal → git → session → settings
|
||||
3. Extract page layout styles
|
||||
4. Delete `styles.css`
|
||||
5. Run full visual check
|
||||
|
||||
**Quality gate:** `npm run build` succeeds, `npm run lint` passes, manual visual verification of all pages
|
||||
|
||||
### Phase 3a: Backend Shared Dependencies
|
||||
|
||||
**Goal:** Extract duplicated auth helpers so later router splits don't duplicate them.
|
||||
|
||||
| Action | Old | New | Technique |
|
||||
|--------|-----|-----|-----------|
|
||||
| Extract `get_current_user` | `api/tool_instances.py`, `api/git_repositories.py`, etc. | `auth/dependencies.py` | Find all `_get_user` functions, unify, move |
|
||||
| Extract `get_owned_project` | Same routers | `auth/dependencies.py` | Same |
|
||||
| Extract `get_owned_repository` | Same routers | `auth/dependencies.py` | Same |
|
||||
| Update router imports | inline helper | `from ..auth.dependencies import get_current_user` | Import rewrite |
|
||||
|
||||
**Quality gate:** `pytest` passes, all integration tests pass
|
||||
|
||||
### Phase 3b: `tool_instances.py` Decomposition
|
||||
|
||||
**Goal:** Split the 1,463-line monster into router + services + schemas.
|
||||
|
||||
| Action | Old | New | Technique |
|
||||
|--------|-----|-----|-----------|
|
||||
| Create schemas | Inline Pydantic models in router | `schemas/tool_instance.py` | Extract `CreateInstanceRequest`, `InstanceResponse`, etc. |
|
||||
| Extract compose logic | `tool_instances.py` `_modify_compose_file` | `services/docker/compose.py` | Cut-paste + tests |
|
||||
| Extract container lifecycle | `tool_instances.py` start/stop/restart | `services/docker/container.py` | Cut-paste |
|
||||
| Extract tunnel logic | `tool_instances.py` recreate-tunnel, health | `services/docker/tunnel.py` | Cut-paste |
|
||||
| Extract config staging | `tool_instances.py` config folder writing | `services/docker/config_staging.py` | Cut-paste |
|
||||
| Extract instance name gen | `tool_instances.py` `_generate_instance_name` | `services/docker/compose.py` or new `services/instances/naming.py` | Cut-paste |
|
||||
| Slim router | 1,463 lines | ~250 lines (endpoints + thin handlers) | Delete moved code, add imports |
|
||||
|
||||
**Quality gate:** `pytest`, especially integration tests for tool instances
|
||||
|
||||
### Phase 3c: `git_repositories.py` + `config_profiles.py` Decomposition
|
||||
|
||||
| Action | Old | New | Technique |
|
||||
|--------|-----|-----|-----------|
|
||||
| Create `schemas/git_repository.py` | Inline in router | New file | Extract |
|
||||
| Create `schemas/config_profile.py` | Inline in router | New file | Extract |
|
||||
| Extract file browsing endpoints | `git_repositories.py` | `api/git_files.py` (or keep in router but delegate) | Move endpoint handlers |
|
||||
| Extract git control endpoints | `git_repositories.py` | Keep in router but delegate to `services/git/control.py` | Thin handlers |
|
||||
| Extract config profile resolution | `config_profiles.py` | `services/profile_resolver.py` (already exists, use it more) | Refactor to use existing service |
|
||||
| Slim routers | 900 + 877 lines | ~250 lines each | Delete moved code |
|
||||
|
||||
**Quality gate:** `pytest`, git-related integration tests
|
||||
|
||||
### Phase 4a: `tool-workshop` Page Split
|
||||
|
||||
**Goal:** Split the 700-line page into tab components.
|
||||
|
||||
| Action | Old | New | Technique |
|
||||
|--------|-----|-----|-----------|
|
||||
| Extract `ToolTypesTab` | `pages/tool-workshop.tsx` (inline state + JSX) | `components/features/tool-workshop/ToolTypesTab.tsx` | Cut-paste |
|
||||
| Extract `ToolConfigsTab` | Same | `components/features/tool-workshop/ToolConfigsTab.tsx` | Cut-paste |
|
||||
| Extract `ConfigFoldersTab` | Same | `components/features/tool-workshop/ConfigFoldersTab.tsx` | Cut-paste |
|
||||
| Slim page | ~700 lines | ~100 lines (tab switcher + layout) | Compose tabs |
|
||||
| Create barrel | — | `components/features/tool-workshop/index.ts` | New |
|
||||
|
||||
**Quality gate:** `tsc`, `eslint`, manual test of all 3 tabs
|
||||
|
||||
### Phase 4b: Pages Split
|
||||
|
||||
| Action | Old | New | Technique |
|
||||
|--------|-----|-----|-----------|
|
||||
| Extract `SessionList`, `SessionCard`, `CreateSessionForm` | `pages/sessions.tsx` | `components/features/session/` | Cut-paste |
|
||||
| Extract `DashboardSummary`, `QuickActions` | `pages/dashboard.tsx` | `components/features/dashboard/` | Cut-paste |
|
||||
| Rename pages | `dashboard.tsx` | `DashboardPage.tsx` | `git mv` |
|
||||
| Rename pages | `git-history.tsx` | `GitHistoryPage.tsx` | `git mv` |
|
||||
| Rename pages | `repo-workspace.tsx` | `RepoWorkspacePage.tsx` | `git mv` |
|
||||
| etc. | all pages | PascalCase matching component | `git mv` |
|
||||
|
||||
**Quality gate:** `tsc`, `eslint`, router still resolves all routes
|
||||
|
||||
### Phase 4c: Naming Consistency
|
||||
|
||||
| Action | Old | New | Technique |
|
||||
|--------|-----|-----|-----------|
|
||||
| Rename component files | `app-shell.tsx` | `AppShell.tsx` | `git mv` |
|
||||
| Rename component files | `git-toolbar.tsx` | `GitToolbar.tsx` | `git mv` |
|
||||
| Rename component files | `file-editor.tsx` | `FileEditor.tsx` | `git mv` |
|
||||
| Rename component files | `instance-list.tsx` | `InstanceList.tsx` | `git mv` |
|
||||
| Rename component files | `terminal.tsx` | `TerminalComponent.tsx` | `git mv` |
|
||||
| Rename API files | `tool_types.ts` | `tool-types.ts` | `git mv` |
|
||||
| Rename API files | `git_repositories.ts` | `git-repositories.ts` | `git mv` |
|
||||
| Rename API files | `config_folders.ts` | `config-folders.ts` | `git mv` |
|
||||
| Update all imports | old paths | new paths | IDE refactor / sed |
|
||||
| Update router | old page paths | new page paths | Edit `router.tsx` |
|
||||
|
||||
**Quality gate:** `tsc`, `eslint`, all tests pass
|
||||
|
||||
### Phase 5: Tests + Docs
|
||||
|
||||
| Action | Description |
|
||||
|--------|-------------|
|
||||
| Add tests for `FileBrowser` | Basic render + interaction tests |
|
||||
| Add tests for `LoadingState`, `ErrorState` | Render tests |
|
||||
| Add tests for extracted tabs | `ToolTypesTab`, `ToolConfigsTab`, `ConfigFoldersTab` |
|
||||
| Write `docs/development/naming.md` | Document all naming conventions |
|
||||
| Dead code cleanup | Remove unused CSS classes, unused exports |
|
||||
| Final quality gate | Full `tsc`, `eslint`, `pytest`, build, visual check |
|
||||
|
||||
---
|
||||
|
||||
## 6. Risk Mitigation by Phase
|
||||
|
||||
### Phase 1 (Safe Foundations)
|
||||
- **Risk:** Type extraction breaks consumers
|
||||
- **Mitigation:** Update ALL consumers in the same commit; run `tsc` before commit
|
||||
|
||||
### Phase 2 (Style System)
|
||||
- **Risk:** Visual regressions from CSS split
|
||||
- **Mitigation:** Keep original `styles.css` until all extractions are verified; delete only at phase end
|
||||
|
||||
### Phase 3 (Backend Decomposition)
|
||||
- **Risk:** Endpoint behavior changes during router slimming
|
||||
- **Mitigation:** Pure cut-paste with zero logic changes; integration tests verify behavior
|
||||
|
||||
### Phase 4 (Frontend Pages)
|
||||
- **Risk:** Router breaks from file renames
|
||||
- **Mitigation:** Update `router.tsx` in the same commit as renames; `git mv` preserves history
|
||||
|
||||
### Phase 5 (Tests + Docs)
|
||||
- **Risk:** Low — additive only
|
||||
|
||||
---
|
||||
|
||||
## 7. Tooling Recommendations
|
||||
|
||||
### Import Rewriting
|
||||
Use VS Code / Vite path aliases to minimize import churn:
|
||||
```json
|
||||
// tsconfig.json (already configured)
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
```
|
||||
|
||||
### Automated Refactoring
|
||||
- **File moves:** `git mv` (preserves git history)
|
||||
- **Import updates:** VS Code "Move to new file" or find-replace with path patterns
|
||||
- **Dead CSS detection:** `purgecss` or manual grep — run after Phase 2
|
||||
|
||||
### Verification Scripts
|
||||
```bash
|
||||
# File size check
|
||||
find apps/web/src apps/api/src -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.py" -o -name "*.css" \) -exec sh -c 'lines=$(wc -l < "$1"); if [ "$lines" -gt 300 ]; then echo "OVERSIZED ($lines): $1"; fi' _ {} \;
|
||||
|
||||
# Inner component check
|
||||
grep -rn "const [A-Z].*=" apps/web/src/pages/ || echo "No inner components found"
|
||||
|
||||
# CSS module check
|
||||
find apps/web/src/components -name "*.module.css" | wc -l
|
||||
|
||||
# Barrel export check
|
||||
test -f apps/web/src/types/index.ts && echo "types barrel exists"
|
||||
test -f apps/web/src/components/ui/index.ts && echo "ui barrel exists"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Definition of Done (Per Phase)
|
||||
|
||||
Each phase is done when:
|
||||
1. All files in the phase are ≤ 300 lines
|
||||
2. `tsc --noEmit` passes
|
||||
3. `eslint` passes
|
||||
4. `pytest` passes (backend phases) or `vitest run` passes (frontend phases)
|
||||
5. No visual regressions (frontend phases)
|
||||
6. Commit uses `git mv` for moves (preserves history)
|
||||
7. Commit message references this SDD change: `refactor: phase N — description`
|
||||
|
||||
---
|
||||
|
||||
*Design prepared for SDD tasks phase. Next: break into reviewable implementation tasks with line-count forecasts.*
|
||||
@@ -0,0 +1,532 @@
|
||||
# Repo Restructure — Exploration Report
|
||||
|
||||
**Project:** Headquarter (full-stack workspace platform)
|
||||
**Date:** 2026-06-02
|
||||
**Scope:** Comprehensive codebase audit for structural refactoring
|
||||
|
||||
---
|
||||
|
||||
## 1. Directory Structure
|
||||
|
||||
### Root Layout
|
||||
```
|
||||
/workspace
|
||||
├── apps/
|
||||
│ ├── web/ # React 18 + Vite frontend
|
||||
│ └── api/ # Python FastAPI + SQLAlchemy backend
|
||||
├── e2e/ # Playwright tests
|
||||
├── docs/ # (not heavily populated)
|
||||
└── openspec/ # OpenSpec changes
|
||||
```
|
||||
|
||||
### Frontend (`apps/web/src/`)
|
||||
```
|
||||
src/
|
||||
├── api/ # 13 API modules (~1,200 LOC total)
|
||||
│ ├── client.ts
|
||||
│ ├── dashboard.ts
|
||||
│ ├── git_repositories.ts
|
||||
│ ├── profile.ts
|
||||
│ ├── projects.ts
|
||||
│ ├── sessions.ts
|
||||
│ ├── settings.ts
|
||||
│ ├── ssh_keys.ts
|
||||
│ ├── terminal.ts
|
||||
│ ├── tool_configs.ts
|
||||
│ ├── tool_types.ts
|
||||
│ ├── config_folders.ts
|
||||
│ └── config_profiles.ts
|
||||
├── components/ # 16 components (~2,100 LOC total)
|
||||
│ ├── app-shell.tsx
|
||||
│ ├── code-editor.tsx
|
||||
│ ├── commit-dialog.tsx
|
||||
│ ├── commit-panel.tsx
|
||||
│ ├── file-editor.tsx
|
||||
│ ├── git-toolbar.tsx
|
||||
│ ├── icon.tsx
|
||||
│ ├── instance-list.tsx
|
||||
│ ├── merge-dialog.tsx
|
||||
│ ├── protected-route.tsx
|
||||
│ ├── protected-route.test.tsx
|
||||
│ ├── repository-create-dialog.tsx
|
||||
│ ├── settings-tab-layout.tsx
|
||||
│ ├── syntax-highlighter.tsx
|
||||
│ ├── workspace-header.tsx
|
||||
│ └── repositories-settings-tab.tsx
|
||||
├── hooks/ # 1 hook
|
||||
│ └── use-theme.ts
|
||||
├── pages/ # 15 pages (~3,500 LOC total)
|
||||
│ ├── dashboard.tsx
|
||||
│ ├── git-history.tsx
|
||||
│ ├── git-repositories.tsx
|
||||
│ ├── placeholder.tsx
|
||||
│ ├── profile.tsx
|
||||
│ ├── project-settings.tsx
|
||||
│ ├── projects.tsx
|
||||
│ ├── repo-workspace.tsx
|
||||
│ ├── settings.tsx
|
||||
│ ├── ssh-keys.tsx
|
||||
│ ├── terminal.tsx
|
||||
│ ├── tool-configs.tsx
|
||||
│ ├── tool-types.tsx
|
||||
│ └── tool-workshop.tsx
|
||||
├── state/ # 2 context providers
|
||||
│ ├── auth.tsx
|
||||
│ └── sessions.tsx
|
||||
├── types/ # 2 type modules
|
||||
│ └── terminal.ts
|
||||
├── utils/ # 3 utilities
|
||||
│ ├── icons.ts
|
||||
│ ├── language.ts
|
||||
│ └── terminal-protocol.ts
|
||||
├── styles.css # 1 massive stylesheet (2,844 lines)
|
||||
├── router.tsx # Route definitions
|
||||
├── main.tsx # Entry point
|
||||
└── types.ts # Shared domain types
|
||||
```
|
||||
|
||||
### Backend (`apps/api/`)
|
||||
```
|
||||
apps/api/
|
||||
├── src/
|
||||
│ ├── main.py # App entry point (~287 lines)
|
||||
│ ├── config.py # Pydantic settings (~128 lines)
|
||||
│ ├── database.py # SQLAlchemy setup (~114 lines)
|
||||
│ ├── logging_config.py # Middleware & logging (~92 lines)
|
||||
│ ├── auth/
|
||||
│ │ ├── session.py
|
||||
│ │ └── dependencies.py
|
||||
│ ├── api/ # 15 routers
|
||||
│ │ ├── auth.py
|
||||
│ │ ├── config_folders.py
|
||||
│ │ ├── config_profiles.py
|
||||
│ │ ├── dashboard.py
|
||||
│ │ ├── git_repositories.py # ~900+ lines
|
||||
│ │ ├── health.py
|
||||
│ │ ├── instance_proxy.py
|
||||
│ │ ├── projects.py
|
||||
│ │ ├── ssh_keys.py
|
||||
│ │ ├── terminal.py
|
||||
│ │ ├── tool_configs.py
|
||||
│ │ ├── tool_instances.py # ~1,463 lines — CRITICAL
|
||||
│ │ ├── tool_types.py
|
||||
│ │ ├── user_config.py
|
||||
│ │ └── users.py
|
||||
│ ├── models/ # SQLAlchemy models
|
||||
│ ├── services/ # Business logic
|
||||
│ │ ├── docker.py # ~457+ lines
|
||||
│ │ ├── docker_build.py
|
||||
│ │ ├── git_control.py
|
||||
│ │ ├── git_files.py
|
||||
│ │ ├── git_history.py
|
||||
│ │ ├── git_url_parser.py
|
||||
│ │ ├── profile_resolver.py
|
||||
│ │ └── readiness_probe.py
|
||||
│ ├── utils/ # Additional utilities
|
||||
│ └── scripts/
|
||||
│ └── seed.py
|
||||
├── alembic/versions/ # 14+ migrations
|
||||
└── tests/
|
||||
├── conftest.py
|
||||
├── unit/
|
||||
└── integration/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. File Sizes — Files Over 200 Lines
|
||||
|
||||
### 🔴 CRITICAL — Over 400 Lines (Must Split)
|
||||
|
||||
| File | Lines | Issue |
|
||||
|------|-------|-------|
|
||||
| `apps/web/src/styles.css` | **2,844** | Single stylesheet for entire app; mixes layout, components, pages, syntax highlighting, and themes |
|
||||
| `apps/api/src/api/tool_instances.py` | **1,463** | Monolithic router: CRUD, Docker orchestration, tunneling, proxying, config resolution, readiness probes |
|
||||
| `apps/api/src/api/git_repositories.py` | **~900+** | Combined file browsing, Git control (branch/commit/merge/push/pull), URL parsing, history |
|
||||
| `apps/api/src/services/docker.py` | **~457+** | Docker compose, container management, tunneling, config folder staging all in one |
|
||||
|
||||
### 🟡 WARNING — Over 200 Lines (Should Split)
|
||||
|
||||
| File | Lines | Issue |
|
||||
|------|-------|-------|
|
||||
| `apps/web/src/pages/tool-workshop.tsx` | **~700+** | 3-tab admin page with inline forms for tool types, configs, AND folders |
|
||||
| `apps/web/src/pages/sessions.tsx` | **~668** | Sessions page with create form, active/recent lists, inline confirmations |
|
||||
| `apps/web/src/pages/repo-workspace.tsx` | **~394** | Page + FileBrowser component + mixed data loading |
|
||||
| `apps/web/src/components/instance-list.tsx` | **~388** | Instance CRUD + health checks + create dialog |
|
||||
| `apps/web/src/pages/tool-types.tsx` | **~380** | Tool types list + create/edit dialog inline |
|
||||
| `apps/web/src/pages/tool-configs.tsx` | **~354** | Tool configs list + create/edit dialog inline |
|
||||
| `apps/web/src/hooks/use-terminal-connection.ts` | **~439** | WS lifecycle, ping-pong, reconnection, local echo, resize debouncing |
|
||||
| `apps/web/src/pages/dashboard.tsx` | **~338** | Summary cards, session lists, quick-create form, recent sessions |
|
||||
| `apps/web/src/components/terminal.tsx` | **~309** | Terminal chrome + xterm lifecycle + resize observer |
|
||||
| `apps/web/src/pages/git-history.tsx` | **~233** | Commit list + detail panel with inline formatting |
|
||||
| `apps/web/src/api/git_repositories.ts` | **~245** | API functions + types (reasonable, but types should move) |
|
||||
| `apps/web/src/pages/projects.tsx` | **~206** | List + create dialog + delete confirmation |
|
||||
| `apps/api/src/main.py` | **~287** | Router registration + startup logic + seeding + error handlers |
|
||||
| `apps/api/src/api/config_profiles.py` | **~877** | Config profiles CRUD + complex resolution logic |
|
||||
| `apps/api/src/api/tool_types.py` | **~616** | Tool types CRUD + compose/dockerfile validation |
|
||||
| `apps/api/src/api/config_folders.py` | **~372** | Config folders CRUD |
|
||||
|
||||
---
|
||||
|
||||
## 3. Frontend Module Analysis
|
||||
|
||||
### Components (16 files, ~2,100 LOC, avg ~131 LOC)
|
||||
**Too large:**
|
||||
- `git-toolbar.tsx` (~268) — mixes git ops, branch creation form, merge dialog trigger, status summary
|
||||
- `file-editor.tsx` (~241) — view/edit/commit workflow
|
||||
- `instance-list.tsx` (~388) — instance CRUD + health + create dialog
|
||||
- `terminal.tsx` (~309) — terminal chrome + xterm lifecycle
|
||||
|
||||
**Well-sized:**
|
||||
- `workspace-header.tsx` (~48)
|
||||
- `protected-route.tsx` (~19)
|
||||
- `icon.tsx` (~165)
|
||||
|
||||
### Pages (15 files, ~3,500 LOC, avg ~233 LOC)
|
||||
**All pages are too large.** Every page mixes:
|
||||
- Data fetching (useEffect + API calls)
|
||||
- Local state management (useState for forms, dialogs, loading)
|
||||
- UI rendering (JSX)
|
||||
|
||||
**Worst offenders:**
|
||||
- `tool-workshop.tsx` (~700) — 3 completely different admin interfaces in one file
|
||||
- `sessions.tsx` (~668) — create form + active/recent lists + confirmations
|
||||
- `repo-workspace.tsx` (~394) — contains `FileBrowser` component inline
|
||||
- `dashboard.tsx` (~338) — summary, active sessions, projects list, quick-create form
|
||||
|
||||
### Hooks (3 files)
|
||||
- `use-theme.ts` (~23) — fine
|
||||
- `use-terminal-connection.ts` (~439) — too large; mixes WS lifecycle, ping-pong, reconnection, echo, resize
|
||||
|
||||
### API Modules (13 files, ~1,200 LOC)
|
||||
- Well-organized by domain
|
||||
- **Inconsistency:** Some define types inline (`api/sessions.ts` defines `ToolInstance`, `Session`), others in separate `types.ts`
|
||||
- `api/client.ts` — centralized Axios instance with auth interceptor. Good pattern.
|
||||
|
||||
### State/Context (2 files)
|
||||
- `auth.tsx` (~63) — well-sized
|
||||
- `sessions.tsx` (~44) — well-sized
|
||||
|
||||
### Styles (1 file, 2,844 lines) — CRITICAL
|
||||
**`styles.css` is the biggest problem in the frontend.** It contains:
|
||||
- CSS variables / design tokens
|
||||
- Global resets
|
||||
- Layout (shell, nav, content grid)
|
||||
- Page styles (home, settings, git-history, repo-workspace)
|
||||
- Component styles (cards, buttons, dialogs, forms, file-tree, editor)
|
||||
- Syntax highlighting overrides
|
||||
- Responsive media queries scattered throughout
|
||||
|
||||
### Types
|
||||
- `src/types.ts` — core domain types (SessionUser, Project)
|
||||
- `src/types/terminal.ts` — terminal-specific WebSocket protocol types
|
||||
- **Problem:** API modules also export their own types (`ToolInstance`, `Session`, `GitRepository`, etc.) causing duplication and confusion. `Session` is defined in BOTH `api/sessions.ts` and `state/sessions.tsx`.
|
||||
|
||||
### Utils
|
||||
- `icons.ts` (~180) — icon name mapping
|
||||
- `language.ts` (~90) — file extension → language detection
|
||||
- `terminal-protocol.ts` (~76) — WS message encoding/decoding + type guards
|
||||
|
||||
### Router
|
||||
- `router.tsx` (~58) — clean and readable
|
||||
|
||||
### Tests
|
||||
- `components/protected-route.test.tsx` (~49)
|
||||
- `api/tool_types.test.ts` (~227)
|
||||
- `api/config_folders.test.ts` (~131)
|
||||
- `pages/dashboard.test.tsx` (~81)
|
||||
- `pages/projects.test.tsx` (~174) — failing tests (React Router context issue)
|
||||
- `pages/tool-workshop.test.tsx` (~527)
|
||||
- `hooks/use-terminal-connection.test.ts` (~339)
|
||||
- **Massive gaps:** No tests for most pages, hooks, state providers, or components
|
||||
|
||||
---
|
||||
|
||||
## 4. Backend Module Analysis
|
||||
|
||||
### Entry Points
|
||||
- `src/main.py` (~287) — FastAPI app setup, CORS, middleware, exception handlers, startup events, seeding, router mounting
|
||||
- **Problem:** Seed data (builtin tool types) is hardcoded here (~100 lines of compose templates). Should be in `seeds/` or `services/seed_data.py`.
|
||||
|
||||
### Routers/Endpoints (15 files)
|
||||
**Organization:** One router per domain — good structure in theory, but files are too large.
|
||||
|
||||
**`tool_instances.py` (1,463 lines)** — The worst offender. Contains:
|
||||
- Pydantic request/response models
|
||||
- Helper functions: `_modify_compose_file`, `_apply_resolved_profile`, `_get_user`, `_get_owned_project`, `_sanitize_name`, `_generate_instance_name`
|
||||
- Endpoints: create, list, get, start, stop, restart, delete, logs, recreate-tunnel, health-check, proxy
|
||||
- Inline Docker orchestration logic (should be in services)
|
||||
- Inline config resolution (should use service layer)
|
||||
|
||||
**`git_repositories.py` (~900+ lines)** — Contains:
|
||||
- Repository CRUD
|
||||
- File browsing endpoints
|
||||
- Git control endpoints (branch, checkout, commit, fetch, pull, push, merge)
|
||||
- URL parsing endpoint
|
||||
|
||||
**`config_profiles.py` (~877 lines)** — Contains:
|
||||
- Config profile CRUD
|
||||
- Complex profile resolution logic
|
||||
- Config folder/application logic
|
||||
|
||||
### Models
|
||||
- Located in `src/models/` — one file per entity
|
||||
- Clean separation, well-sized
|
||||
|
||||
### Services/Business Logic
|
||||
- `docker.py` (~456) — Docker compose, container ops, tunneling, config file staging. Too large.
|
||||
- `docker_build.py` (~69) — Image building
|
||||
- `git_control.py` (~295) — Git operations
|
||||
- `git_files.py` (~439) — File tree, read, write
|
||||
- `git_history.py` (~382) — Commit history, graph, diff
|
||||
- `git_url_parser.py` (~228) — URL parsing and validation
|
||||
- `profile_resolver.py` (~251) — Config profile resolution
|
||||
- `readiness_probe.py` (~66) — Container health probes
|
||||
- `terminal_manager.py` (~193) — Terminal session lifecycle
|
||||
- `terminal_session.py` (~162) — Individual terminal session handling
|
||||
|
||||
### Database/ORM
|
||||
- `database.py` (~116) — Engine, session factory, init with alembic subprocess
|
||||
- `config.py` (~143) — Pydantic settings with env var resolution
|
||||
- Alembic migrations in `alembic/versions/` — 14+ migration files
|
||||
|
||||
---
|
||||
|
||||
## 5. Coupling and Dependency Patterns
|
||||
|
||||
### Frontend High-Coupling Files
|
||||
|
||||
**`repo-workspace.tsx`** imports from:
|
||||
- `react-router-dom` (params, search params)
|
||||
- `../api/client` (direct apiClient usage)
|
||||
- `../api/git_repositories`
|
||||
- `../components/commit-panel`
|
||||
- `../components/file-editor`
|
||||
- `../components/git-toolbar`
|
||||
- `../components/instance-list`
|
||||
- `../components/workspace-header`
|
||||
- `../api/tool_types`
|
||||
|
||||
**`dashboard.tsx`** imports from:
|
||||
- `../api/dashboard`, `../api/sessions`, `../api/projects`, `../api/git_repositories`, `../api/tool_types`, `../api/settings`
|
||||
- `../types`, `../components/icon`
|
||||
|
||||
**`tool-workshop.tsx`** imports from:
|
||||
- `../api/tool_types`, `../api/tool_configs`, `../api/config_folders`
|
||||
- Manages 3 separate entity forms with ~20 useState variables each
|
||||
|
||||
### Circular Dependencies
|
||||
- **No obvious circular imports detected**, but `Session` type is duplicated between `api/sessions.ts` and `state/sessions.tsx`, creating conceptual circularity.
|
||||
|
||||
### Business Logic Mixed with UI
|
||||
- **Every page component** contains API calls directly in `useEffect`
|
||||
- Form validation logic is inline in page components
|
||||
- `repo-workspace.tsx` defines `FileBrowser` as an inner component — cannot be tested or reused independently
|
||||
|
||||
### API Call Patterns
|
||||
- **Mostly centralized** in `api/` modules — good
|
||||
- **Exception:** `repo-workspace.tsx`, `file-editor.tsx`, `project-settings.tsx` use `apiClient` directly instead of domain API modules
|
||||
- **Exception:** `app-shell.tsx` calls `getUserSessions()` directly
|
||||
|
||||
---
|
||||
|
||||
## 6. Naming Inconsistencies
|
||||
|
||||
### File Naming Conventions
|
||||
|
||||
| Location | Convention | Examples | Issues |
|
||||
|----------|-----------|----------|--------|
|
||||
| `pages/` | mostly kebab-case | `git-history.tsx`, `repo-workspace.tsx` | `projects.tsx`, `profile.tsx`, `settings.tsx`, `dashboard.tsx` are NOT kebab-case |
|
||||
| `components/` | kebab-case | `app-shell.tsx`, `protected-route.tsx` | `repositories-settings-tab.tsx` (long but consistent) |
|
||||
| `api/` | snake_case | `tool_configs.ts`, `git_repositories.ts` | Mixes with frontend convention |
|
||||
| `utils/` | kebab-case | `terminal-protocol.ts` | Good |
|
||||
| `hooks/` | camelCase | `useTheme.ts` would be standard, but file is `use-theme.ts` | Actually kebab-case, which is fine but inconsistent with React convention |
|
||||
| Backend routers | snake_case | `tool_instances.py`, `git_repositories.py` | Consistent within backend |
|
||||
| Backend services | snake_case | `docker.py`, `profile_resolver.py` | Consistent |
|
||||
|
||||
### Component vs File Naming
|
||||
- Component `ProtectedRoute` → file `protected-route.tsx` ✅
|
||||
- Component `AppShell` → file `app-shell.tsx` ✅
|
||||
- Component `GitHistoryPage` → file `git-history.tsx` ❌ (should be `GitHistoryPage` in `git-history-page.tsx` OR component renamed to `GitHistory`)
|
||||
- Component `RepoWorkspace` → file `repo-workspace.tsx` ❌ (same issue)
|
||||
- Page components use `Page` suffix inconsistently: `ProjectsPage`, `GitHistoryPage`, but `RepoWorkspace` has no `Page` suffix
|
||||
|
||||
### Function/Variable Naming
|
||||
- Frontend: camelCase consistently
|
||||
- Backend: snake_case consistently
|
||||
- **API types:** Backend uses `snake_case` fields; frontend types mirror this (`default_ssh_key_id`, `tool_type_name`). Good for API alignment.
|
||||
|
||||
---
|
||||
|
||||
## 7. Quality Signals
|
||||
|
||||
### TODO/FIXME Comments
|
||||
- Only **2 TODOs** found:
|
||||
- `apps/api/src/utils/git_history.py:188-189`: `# TODO: extract committer separately` (appears twice)
|
||||
|
||||
This is surprisingly low — suggests either good maintenance or lack of inline documentation.
|
||||
|
||||
### Dead Code / Unused Exports
|
||||
- `dashboard.tsx` exports `HomePage as DashboardPage` — dual naming is confusing
|
||||
- `src/types.ts` exports `SessionPayload` which is only used in auth context
|
||||
- Several CSS classes in `styles.css` may be unused (hard to verify without build analysis)
|
||||
|
||||
### Duplicate Logic
|
||||
- **Backend auth checks:** `_get_user()` and `_get_owned_project()` are duplicated in nearly every router file (`tool_instances.py`, `git_repositories.py`, `ssh_keys.py`, etc.)
|
||||
- **Frontend loading/error patterns:** Identical `status: "loading" | "ready" | "error"` state + retry button pattern copied in ~8 page components
|
||||
- **Frontend form dialogs:** Create/edit/delete confirmation pattern repeated in `projects.tsx`, `tool-types.tsx`, `tool-configs.tsx`, `ssh-keys.tsx`
|
||||
|
||||
### Test Coverage Gaps
|
||||
- **Frontend:** 7 test files, but many pages and components untested
|
||||
- **Backend:** Unit tests for `git_url_parser.py`, `migration_metadata.py`, `profile_resolver.py`, `readiness_probe.py`, `docker_build.py`, `terminal_manager.py`, `terminal_session.py`; integration tests via `conftest.py`
|
||||
- **E2E tests** only cover login flow (`e2e/tests/login.spec.ts`)
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Target Directory Structure
|
||||
|
||||
#### Frontend (`apps/web/src/`)
|
||||
```
|
||||
src/
|
||||
├── api/ # Keep — centralized API layer
|
||||
│ ├── client.ts
|
||||
│ ├── __mocks__/ # Add: mock API responses for tests
|
||||
│ └── {domain}/ # Group by domain
|
||||
│ ├── index.ts # Re-exports
|
||||
│ ├── types.ts # Domain types ONLY
|
||||
│ └── api.ts # API functions
|
||||
├── components/ # Generic UI components
|
||||
│ ├── ui/ # Primitive components (Button, Card, Dialog, Input)
|
||||
│ ├── layout/ # AppShell, Navigation, Header
|
||||
│ └── features/ # Domain-specific components
|
||||
│ ├── git/
|
||||
│ ├── project/
|
||||
│ ├── session/
|
||||
│ └── settings/
|
||||
├── hooks/ # Custom hooks
|
||||
│ ├── use-theme.ts
|
||||
│ ├── use-auth.ts # Extract from state/auth.tsx?
|
||||
│ └── use-api-query.ts # NEW: reusable data fetching
|
||||
├── pages/ # Route entry points ONLY
|
||||
│ ├── dashboard/
|
||||
│ │ └── page.tsx
|
||||
│ ├── projects/
|
||||
│ │ ├── page.tsx
|
||||
│ │ ├── project-list.tsx
|
||||
│ │ └── create-project-dialog.tsx
|
||||
│ └── ...
|
||||
├── state/ # Keep contexts
|
||||
├── styles/
|
||||
│ ├── tokens.css # CSS variables only
|
||||
│ ├── global.css # Resets + base styles
|
||||
│ ├── components/ # Component styles
|
||||
│ └── pages/ # Page-specific styles
|
||||
├── types/ # Centralize ALL shared types
|
||||
│ └── index.ts
|
||||
└── utils/
|
||||
```
|
||||
|
||||
#### Backend (`apps/api/src/`)
|
||||
```
|
||||
src/
|
||||
├── main.py # Router mounting + middleware ONLY
|
||||
├── config.py
|
||||
├── database.py
|
||||
├── logging_config.py
|
||||
├── auth/
|
||||
├── api/
|
||||
│ └── v1/ # Versioned routes
|
||||
│ ├── __init__.py
|
||||
│ ├── auth.py
|
||||
│ ├── projects/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── router.py
|
||||
│ │ └── dependencies.py
|
||||
│ ├── repositories/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── router.py # CRUD only
|
||||
│ │ ├── files.py # File browsing
|
||||
│ │ └── git.py # Git control operations
|
||||
│ ├── instances/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── router.py # CRUD + lifecycle
|
||||
│ │ ├── compose.py # Compose file generation
|
||||
│ │ ├── tunnel.py # Cloudflare tunnel ops
|
||||
│ │ └── proxy.py # HTTP proxy
|
||||
│ └── ...
|
||||
├── models/
|
||||
├── schemas/ # NEW: Pydantic schemas separate from routers
|
||||
├── services/
|
||||
│ ├── docker/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── compose.py # Extract from docker.py
|
||||
│ │ ├── container.py # Container lifecycle
|
||||
│ │ ├── tunnel.py # Cloudflare tunneling
|
||||
│ │ └── config.py # Config file staging
|
||||
│ └── git/
|
||||
│ ├── control.py
|
||||
│ ├── files.py
|
||||
│ └── history.py
|
||||
├── seeds/ # NEW: Seed data
|
||||
│ └── builtin_tool_types.py
|
||||
└── tests/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Files That MUST Be Split
|
||||
|
||||
1. **`apps/web/src/styles.css`** → Split into 5-8 files by concern
|
||||
2. **`apps/api/src/api/tool_instances.py`** → Split into router + compose service + tunnel service + proxy service
|
||||
3. **`apps/api/src/api/git_repositories.py`** → Split into repository CRUD router + file router + git control router
|
||||
4. **`apps/api/src/services/docker.py`** → Split into compose, container, tunnel, config staging modules
|
||||
5. **`apps/web/src/pages/tool-workshop.tsx`** → Split into 3 page tabs or feature components
|
||||
6. **`apps/web/src/pages/repo-workspace.tsx`** → Extract `FileBrowser` to `components/features/git/file-browser.tsx`
|
||||
7. **`apps/web/src/pages/sessions.tsx`** → Extract create form, active list, recent list
|
||||
8. **`apps/web/src/hooks/use-terminal-connection.ts`** → Extract WS manager, echo handler, resize debouncer
|
||||
|
||||
---
|
||||
|
||||
### Naming Convention to Standardize On
|
||||
|
||||
| Layer | Convention | Example |
|
||||
|-------|-----------|---------|
|
||||
| React components (files) | PascalCase matching component | `GitHistoryPage.tsx` |
|
||||
| React hooks (files) | camelCase | `useTheme.ts` |
|
||||
| Utility modules | kebab-case | `terminal-protocol.ts` |
|
||||
| API modules | kebab-case | `tool-configs.ts` |
|
||||
| Backend routers | snake_case | `tool_instances.py` |
|
||||
| Backend services | snake_case | `profile_resolver.py` |
|
||||
| CSS modules | kebab-case matching component | `git-history-page.module.css` |
|
||||
|
||||
---
|
||||
|
||||
### Order of Migration (First → Last)
|
||||
|
||||
**Phase 1: Safe Foundations (low risk)**
|
||||
1. Extract shared types to `src/types/index.ts` (remove duplication)
|
||||
2. Create `src/hooks/use-api-query.ts` for reusable data fetching
|
||||
3. Extract `FileBrowser` from `repo-workspace.tsx`
|
||||
4. Move seed data from `main.py` to `seeds/builtin_tool_types.py`
|
||||
|
||||
**Phase 2: Style System (medium risk, high reward)**
|
||||
5. Split `styles.css` into design tokens + component modules
|
||||
6. Introduce CSS modules or Tailwind utility extraction for component styles
|
||||
|
||||
**Phase 3: Backend Decomposition (medium risk)**
|
||||
7. Extract `_get_user` and `_get_owned_project` to `auth/dependencies.py` or `api/dependencies.py`
|
||||
8. Split `tool_instances.py` into router + services
|
||||
9. Split `git_repositories.py` into CRUD + files + git control routers
|
||||
10. Split `services/docker.py` into focused modules
|
||||
|
||||
**Phase 4: Frontend Page Decomposition (higher risk — touches UX)**
|
||||
11. Split `tool-workshop.tsx` into feature components
|
||||
12. Split `dashboard.tsx` into summary/session/project sections
|
||||
13. Split `sessions.tsx` into create-form + lists
|
||||
14. Split `settings.tsx` — move `GeneralSettingsTab` to its own file
|
||||
|
||||
**Phase 5: Testing & Polish**
|
||||
15. Add tests for extracted components
|
||||
16. Add backend integration tests for refactored routers
|
||||
@@ -0,0 +1,172 @@
|
||||
# SDD Proposal: Repository Restructuring and Modularization
|
||||
|
||||
## Overview
|
||||
|
||||
The Headquarter codebase has grown organically over ~6 months of active development. What began as a lean full-stack application has accumulated structural debt: monolithic files, mixed concerns, duplicated types, inconsistent naming, and a single 2,844-line stylesheet. This proposal plans a phased refactoring to establish clear module boundaries, enforce a ~200-line-per-file target (hard limit 300), and standardize naming conventions across the entire repo.
|
||||
|
||||
**Motivation:**
|
||||
- Files over 400 lines are difficult to reason about, test, and review
|
||||
- Pages mix data fetching, state management, form logic, and UI rendering
|
||||
- A single stylesheet makes theme changes risky and component isolation impossible
|
||||
- Backend routers contain business logic that should live in services
|
||||
- Duplicate types (`Session`, `ToolInstance`) create drift between API and state layers
|
||||
- Naming inconsistencies make file discovery harder for new contributors
|
||||
|
||||
**Desired outcome:** A codebase where every file has a single, obvious responsibility; imports follow predictable patterns; and a new developer can locate any functionality within 30 seconds.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
1. **Frontend type consolidation**
|
||||
- Move all domain types from `api/*.ts` into `types/` with clear domain grouping
|
||||
- Remove duplication between `api/sessions.ts` and `state/sessions.tsx`
|
||||
- Standardize type naming and export patterns
|
||||
|
||||
2. **Frontend page decomposition**
|
||||
- Extract inline components (e.g., `FileBrowser` from `repo-workspace.tsx`)
|
||||
- Split "list + form + dialog" pages into container + presentational components
|
||||
- Extract reusable loading/error/retry UI patterns into shared components
|
||||
|
||||
3. **Frontend style system restructure**
|
||||
- Split `styles.css` into: tokens, global, layout, components, pages, syntax-highlight
|
||||
- Remove unused CSS classes (verified by grep/build)
|
||||
- Keep visual output pixel-identical (no design changes)
|
||||
|
||||
4. **Frontend component organization**
|
||||
- Group domain-specific components under `components/features/{domain}/`
|
||||
- Keep generic UI primitives at `components/ui/`
|
||||
- Rename page component files to match exported names (e.g., `git-history.tsx` → `GitHistoryPage.tsx` or rename component)
|
||||
|
||||
5. **Backend router decomposition**
|
||||
- Extract business logic from `tool_instances.py`, `git_repositories.py`, `config_profiles.py`
|
||||
- Move helper functions (`_get_user`, `_get_owned_project`) to shared dependencies
|
||||
- Split large routers by sub-resource (CRUD vs. operations vs. files)
|
||||
|
||||
6. **Backend service decomposition**
|
||||
- Split `services/docker.py` into compose, container, tunnel, config-staging modules
|
||||
- Ensure no service module exceeds 300 lines
|
||||
|
||||
7. **Backend seed data extraction**
|
||||
- Move hardcoded seed data from `main.py` to `seeds/builtin_tool_types.py`
|
||||
|
||||
8. **Naming convention standardization**
|
||||
- Frontend React components: PascalCase files matching component name
|
||||
- Frontend hooks: camelCase (`useTheme.ts`)
|
||||
- Frontend utilities/api: kebab-case
|
||||
- Backend modules: snake_case
|
||||
- Document conventions in `docs/development/naming.md`
|
||||
|
||||
### Out of Scope (Non-Goals)
|
||||
|
||||
1. **No behavior changes** — All user-facing functionality stays identical; this is pure restructuring
|
||||
2. **No new features** — We are not adding capabilities, only reorganizing existing ones
|
||||
3. **No technology swaps** — Keeping React 18, Vite, FastAPI, SQLAlchemy, xterm as-is
|
||||
4. **No test rewrites** — Existing tests should pass after path updates; we are not changing test frameworks or strategies
|
||||
5. **No database migrations** — Model files stay in place; only code organization changes
|
||||
6. **No build system changes** — Keep existing vite.config.ts, tsconfig.json, pyproject.toml
|
||||
7. **No CI/CD changes** — Existing quality gates (typecheck, lint, pytest) must continue to pass
|
||||
8. **No documentation overhaul** — We will add a naming conventions doc, but not rewrite all docs
|
||||
|
||||
---
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|------------|
|
||||
| Import path breakage | High | Medium | Use IDE/automated refactor for import rewrites; run full typecheck after every phase |
|
||||
| CSS regression | Medium | High | Split styles incrementally; verify each page visually after each CSS file split; keep original styles.css as backup during migration |
|
||||
| Lost git history | Medium | Low | Use `git mv` for file moves; avoid copy-delete patterns |
|
||||
| Test failures from path changes | High | Low | Update test imports alongside source imports; run test suite after each phase |
|
||||
| Scope creep | Medium | High | Strict non-goals list; pause between phases; require explicit approval to expand scope |
|
||||
| Merge conflicts with active development | Medium | High | Coordinate timing; prefer short phases with quick PRs; avoid refactoring files with active feature branches |
|
||||
| Reviewer fatigue | Medium | Medium | Auto-forecast at 400 lines; split into chained PRs; each PR limited to one concern |
|
||||
| Accidental behavior change | Low | High | Pure cut-paste with no logic changes; reviewer checks for any non-import diffs |
|
||||
|
||||
---
|
||||
|
||||
## High-Level Approach
|
||||
|
||||
We will execute in **5 phases**, each producing an independent, reviewable PR:
|
||||
|
||||
### Phase 1: Safe Foundations (est. +200/-150 lines, 1 PR)
|
||||
- Consolidate types: create `types/index.ts` with all domain types
|
||||
- Update imports in all consumers
|
||||
- Extract `FileBrowser` from `repo-workspace.tsx`
|
||||
- Move seed data from `main.py` to `seeds/`
|
||||
- Extract shared auth dependencies
|
||||
|
||||
### Phase 2: Style System Restructure (est. +50/-2,700 lines, 1 PR)
|
||||
- Split `styles.css` into 6 files under `styles/`
|
||||
- Update `main.tsx` to import new style entry point
|
||||
- Verify no visual regressions
|
||||
|
||||
### Phase 3: Backend Router Decomposition (est. +800/-1,500 lines, 2-3 chained PRs)
|
||||
- PR 3a: Extract shared dependencies and helpers
|
||||
- PR 3b: Split `tool_instances.py` → router + services
|
||||
- PR 3c: Split `git_repositories.py` and `config_profiles.py`
|
||||
|
||||
### Phase 4: Frontend Page Decomposition (est. +600/-1,200 lines, 2-3 chained PRs)
|
||||
- PR 4a: Split `tool-workshop.tsx` into feature components
|
||||
- PR 4b: Split `sessions.tsx`, `dashboard.tsx`, `repo-workspace.tsx`
|
||||
- PR 4c: Rename page components and files for consistency
|
||||
|
||||
### Phase 5: Testing & Polish (est. +300/-50 lines, 1 PR)
|
||||
- Add tests for extracted components
|
||||
- Document naming conventions
|
||||
- Final cleanup: remove dead code, unused exports
|
||||
|
||||
**Total estimated churn:** ~2,000 lines added, ~5,700 lines removed (net: files become smaller and more numerous)
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### Overall
|
||||
- [ ] No file in `src/` exceeds 300 lines (exceptions: auto-generated migration files)
|
||||
- [ ] `tsc --noEmit` passes with zero errors
|
||||
- [ ] `eslint` passes with zero warnings
|
||||
- [ ] All existing tests pass (frontend: vitest; backend: pytest)
|
||||
- [ ] No visual regressions in key pages (verified manually or via existing e2e)
|
||||
- [ ] No behavior changes — all user flows work identically
|
||||
|
||||
### Per Phase
|
||||
- [ ] Phase 1: All types centralized; zero duplicated type definitions; seed data extracted
|
||||
- [ ] Phase 2: `styles.css` deleted; styles split by concern; no visual regressions
|
||||
- [ ] Phase 3: No router exceeds 300 lines; business logic lives in services; no inline Docker/git ops in routers
|
||||
- [ ] Phase 4: No page exceeds 300 lines; inline components extracted; naming consistent
|
||||
- [ ] Phase 5: Naming convention doc exists; extracted components have basic tests
|
||||
|
||||
---
|
||||
|
||||
## Review Workload Forecast
|
||||
|
||||
| Phase | Est. Changed Lines | PR Strategy |
|
||||
|-------|-------------------|-------------|
|
||||
| Phase 1 | ~350 | Single PR |
|
||||
| Phase 2 | ~2,750 | Single PR (mostly CSS reorganization) |
|
||||
| Phase 3a | ~400 | Single PR |
|
||||
| Phase 3b | ~800 | Single PR |
|
||||
| Phase 3c | ~700 | Single PR |
|
||||
| Phase 4a | ~500 | Single PR |
|
||||
| Phase 4b | ~600 | Single PR |
|
||||
| Phase 4c | ~350 | Single PR |
|
||||
| Phase 5 | ~350 | Single PR |
|
||||
|
||||
**All PRs are under the 400-line review budget.** Phases 2 and 3/4 may require careful review focus due to file move volume, but each PR stays within the limit.
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. Should we adopt CSS Modules for component styles, or keep global CSS with BEM-like naming?
|
||||
2. Should backend routers be versioned under `api/v1/` now, or keep flat `api/` structure?
|
||||
3. Should extracted frontend feature components live in `components/features/` or `features/` at root?
|
||||
4. Do we want to introduce barrel exports (`index.ts`) for each domain module?
|
||||
5. Should we run this refactor in a feature branch, or merge each phase to main immediately?
|
||||
|
||||
---
|
||||
|
||||
*Proposal prepared for SDD review. Next phase: Spec writing with detailed requirements and scenarios.*
|
||||
@@ -0,0 +1,329 @@
|
||||
# Spec: Repository Restructuring and Modularization
|
||||
|
||||
## Overview
|
||||
|
||||
Restructure the Headquarter monorepo into a modular, maintainable architecture where every source file has a single responsibility and stays within 300 lines (target: 100–200). No behavior changes. No new features. Pure structural reorganization with standardized naming conventions.
|
||||
|
||||
**Scope:** Frontend (`apps/web/src/`) and backend (`apps/api/src/`)
|
||||
**Non-goals:** Technology swaps, feature additions, database migrations, CI/CD changes
|
||||
**Target file size:** 100–200 lines; hard limit 300 lines
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions (MUST follow)
|
||||
|
||||
| Layer | File Naming | Component/Function Naming | Example |
|
||||
|-------|------------|--------------------------|---------|
|
||||
| React page components | PascalCase matching exported name | `GitHistoryPage` | `GitHistoryPage.tsx` |
|
||||
| React feature components | PascalCase matching exported name | `FileBrowser` | `FileBrowser.tsx` + `FileBrowser.module.css` |
|
||||
| React UI primitives | PascalCase matching exported name | `Button`, `Dialog` | `Button.tsx` + `Button.module.css` |
|
||||
| React hooks | camelCase | `useTheme`, `useApiQuery` | `useTheme.ts` |
|
||||
| Frontend API modules | kebab-case | — | `tool-configs.ts` |
|
||||
| Frontend utilities | kebab-case | camelCase functions | `terminal-protocol.ts` |
|
||||
| Frontend types | kebab-case | PascalCase interfaces | `session.ts` |
|
||||
| CSS modules | kebab-case matching component | — | `file-browser.module.css` |
|
||||
| Backend routers | snake_case | snake_case handlers | `tool_instances.py` |
|
||||
| Backend services | snake_case | snake_case functions | `docker_compose.py` |
|
||||
| Backend models | snake_case | PascalCase classes | `tool_instance.py` |
|
||||
| Backend tests | snake_case prefixed with `test_` | — | `test_tool_instances.py` |
|
||||
|
||||
**CSS Modules rule:** Every React component with significant styling gets its own `.module.css` file. Global styles live in `styles/` and only contain resets, tokens, and layout foundations.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### AC-1: No Monolithic Files Remain
|
||||
|
||||
**GIVEN** the codebase after restructuring
|
||||
**WHEN** we count lines in every `.ts`, `.tsx`, `.py`, and `.css` file under `src/`
|
||||
**THEN** no file exceeds 300 lines
|
||||
**AND** the average file size under each domain directory is under 200 lines
|
||||
|
||||
**Test:** Run `find src -type f | xargs wc -l | sort -rn` — verify top result ≤ 300.
|
||||
|
||||
### AC-2: Types Are Centralized and Deduplicated
|
||||
|
||||
**GIVEN** a domain type such as `Session` or `ToolInstance`
|
||||
**WHEN** a developer searches for its definition
|
||||
**THEN** exactly one definition exists under `types/`
|
||||
**AND** `api/sessions.ts` and `state/sessions.tsx` both import from `types/session.ts`
|
||||
**AND** no API module defines types inline
|
||||
|
||||
**Test:** Search for `interface Session` — expect 1 result. Search for `interface ToolInstance` — expect 1 result.
|
||||
|
||||
### AC-3: Styles Are Modular
|
||||
|
||||
**GIVEN** the frontend build
|
||||
**WHEN** `styles.css` is checked
|
||||
**THEN** it does not exist (deleted)
|
||||
**AND** global styles live in `styles/global.css` (resets + tokens + layout)
|
||||
**AND** component styles live in `.module.css` files co-located with components
|
||||
**AND** page styles live in `styles/pages/{page-name}.css` for page-specific layout only
|
||||
**AND** syntax highlighting styles live in `styles/syntax-highlight.css`
|
||||
|
||||
**Test:** `test -f src/styles.css` fails. `find src/styles -name "*.css" | wc -l` ≥ 5.
|
||||
|
||||
### AC-4: Backend Routers Contain Only HTTP Concerns
|
||||
|
||||
**GIVEN** any router file under `api/`
|
||||
**WHEN** reading its contents
|
||||
**THEN** it contains only: route definitions, dependency injection, request/response models, and thin handler functions
|
||||
**AND** no Docker CLI calls, no Git subprocess calls, no file I/O, no compose file mutation
|
||||
**AND** all business logic delegates to `services/` modules
|
||||
|
||||
**Test:** `grep -n "subprocess\|docker\|compose\|os\." apps/api/src/api/*.py` returns zero matches.
|
||||
|
||||
### AC-5: Backend Services Are Focused
|
||||
|
||||
**GIVEN** the `services/` directory
|
||||
**WHEN** listing files
|
||||
**THEN** each service module has a single responsibility (e.g., container lifecycle, tunnel management, compose generation)
|
||||
**AND** `services/docker.py` does not exist (split into focused modules)
|
||||
|
||||
**Test:** `test -f apps/api/src/services/docker.py` fails. Each `.py` in `services/` is ≤ 300 lines.
|
||||
|
||||
### AC-6: Inline Components Are Extracted
|
||||
|
||||
**GIVEN** any page component
|
||||
**WHEN** reading its JSX
|
||||
**THEN** no inner component definitions exist (no `const FileBrowser = () => ...` inside a page)
|
||||
**AND** all extracted components are importable and testable independently
|
||||
|
||||
**Test:** `grep -rn "const [A-Z].*=.*=>" apps/web/src/pages/` returns zero results.
|
||||
|
||||
### AC-7: Naming Is Consistent
|
||||
|
||||
**GIVEN** any source file
|
||||
**WHEN** checking its name against the naming table above
|
||||
**THEN** it follows the convention for its layer
|
||||
**AND** every exported React component matches its file name (case-insensitive)
|
||||
|
||||
**Test:** Script checks that every `.tsx` file's default/named export matches its basename.
|
||||
|
||||
### AC-8: All Quality Gates Pass
|
||||
|
||||
**GIVEN** any phase of the refactor
|
||||
**WHEN** running the quality gates
|
||||
**THEN** `npm run typecheck` (frontend) passes with zero errors
|
||||
**AND** `npm run lint` (frontend) passes with zero warnings
|
||||
**AND** `pytest` (backend) passes with zero failures
|
||||
**AND** no visual regressions are introduced
|
||||
|
||||
**Test:** Run all gates after each phase. No failures.
|
||||
|
||||
### AC-9: Barrel Exports for Stable Boundaries
|
||||
|
||||
**GIVEN** `components/ui/`, `components/features/{domain}/`, or `types/`
|
||||
**WHEN** importing from those directories
|
||||
**THEN** an `index.ts` barrel export exists
|
||||
**AND** consumers import from the directory, not individual files
|
||||
**AND** one-off utilities and API modules do NOT have barrel exports
|
||||
|
||||
**Test:** `test -f src/components/ui/index.ts` passes. `test -f src/utils/index.ts` fails.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### REQ-1: Frontend Type System
|
||||
|
||||
The system SHALL centralize all shared domain types under `apps/web/src/types/`.
|
||||
|
||||
**Rationale:** Prevents drift between API types, state types, and component prop types.
|
||||
|
||||
#### Scenario: Centralizing Session types
|
||||
- **GIVEN** `Session` is defined in `api/sessions.ts` and `state/sessions.tsx`
|
||||
- **WHEN** the refactor is applied
|
||||
- **THEN** a single `types/session.ts` defines the canonical `Session` interface
|
||||
- **AND** both `api/sessions.ts` and `state/sessions.tsx` import from it
|
||||
- **AND** `api/sessions.ts` no longer exports a `Session` type
|
||||
|
||||
#### Scenario: API modules lose inline types
|
||||
- **GIVEN** `api/tool_types.ts` defines `ToolType` inline
|
||||
- **WHEN** the refactor is applied
|
||||
- **THEN** `types/tool-type.ts` defines `ToolType`
|
||||
- **AND** `api/tool_types.ts` imports and re-exports it
|
||||
|
||||
### REQ-2: Frontend Style Modules
|
||||
|
||||
The system SHALL use CSS Modules for component-scoped styles.
|
||||
|
||||
**Rationale:** Eliminates global CSS specificity wars; makes component styles discoverable and deletable.
|
||||
|
||||
#### Scenario: Component with styles
|
||||
- **GIVEN** `FileBrowser` has custom styles
|
||||
- **WHEN** a developer looks for its styles
|
||||
- **THEN** they find `components/features/git/file-browser/FileBrowser.module.css`
|
||||
- **AND** the module contains only `.fileBrowser` and child selectors
|
||||
- **AND** no global class names leak outside the component
|
||||
|
||||
#### Scenario: Global styles remain minimal
|
||||
- **GIVEN** `styles/global.css` exists
|
||||
- **WHEN** reading it
|
||||
- **THEN** it contains only: CSS variables, `* { box-sizing }`, `body` reset, and shell layout grid
|
||||
- **AND** it does not contain component-specific rules (cards, buttons, dialogs, etc.)
|
||||
|
||||
### REQ-3: Backend Router Separation
|
||||
|
||||
The system SHALL separate HTTP routing from business logic.
|
||||
|
||||
**Rationale:** Routers should be thin and testable; business logic should be reusable and independently testable.
|
||||
|
||||
#### Scenario: Creating a tool instance
|
||||
- **GIVEN** a `POST /instances` request
|
||||
- **WHEN** the router handles it
|
||||
- **THEN** it validates the request body with a Pydantic schema
|
||||
- **AND** it calls `services.instances.create_instance(...)`
|
||||
- **AND** it returns the response
|
||||
- **AND** it does not call `docker compose up`, modify files, or manage tunnels
|
||||
|
||||
#### Scenario: Starting a tool instance
|
||||
- **GIVEN** a `POST /instances/{id}/start` request
|
||||
- **WHEN** the router handles it
|
||||
- **THEN** it calls `services.instances.lifecycle.start_instance(...)`
|
||||
- **AND** it does not contain subprocess calls
|
||||
|
||||
### REQ-4: Backend Service Focus
|
||||
|
||||
The system SHALL split `services/docker.py` into single-responsibility modules.
|
||||
|
||||
**Rationale:** Docker operations span compose, containers, tunnels, and config staging — too many concerns for one file.
|
||||
|
||||
#### Scenario: Service decomposition
|
||||
- **GIVEN** the old `services/docker.py`
|
||||
- **WHEN** the refactor is applied
|
||||
- **THEN** the following modules exist:
|
||||
- `services/docker/compose.py` — compose file generation and modification
|
||||
- `services/docker/container.py` — container lifecycle (create, start, stop, remove)
|
||||
- `services/docker/tunnel.py` — Cloudflare tunnel management
|
||||
- `services/docker/config.py` — config folder staging and file writing
|
||||
- **AND** each module is ≤ 300 lines
|
||||
- **AND** `services/docker.py` does not exist
|
||||
|
||||
### REQ-5: Page Component Decomposition
|
||||
|
||||
The system SHALL split page components into route entry points and feature sub-components.
|
||||
|
||||
**Rationale:** Pages should orchestrate data and routing, not contain inline UI implementations.
|
||||
|
||||
#### Scenario: Repo workspace page
|
||||
- **GIVEN** the old `pages/repo-workspace.tsx`
|
||||
- **WHEN** the refactor is applied
|
||||
- **THEN** `pages/repo-workspace/page.tsx` contains only: data loading, layout, and sub-component composition
|
||||
- **AND** `components/features/git/file-browser/FileBrowser.tsx` contains the file tree UI
|
||||
- **AND** `components/features/git/commit-panel/CommitPanel.tsx` contains the commit form
|
||||
- **AND** each extracted component is independently importable
|
||||
|
||||
#### Scenario: Tool workshop page
|
||||
- **GIVEN** the old `pages/tool-workshop.tsx`
|
||||
- **WHEN** the refactor is applied
|
||||
- **THEN** it is split into:
|
||||
- `pages/tool-workshop/page.tsx` — tab navigation and layout
|
||||
- `components/features/tool-workshop/ToolTypesTab.tsx`
|
||||
- `components/features/tool-workshop/ToolConfigsTab.tsx`
|
||||
- `components/features/tool-workshop/ConfigFoldersTab.tsx`
|
||||
- **AND** each tab component manages its own form state
|
||||
|
||||
### REQ-6: Inline Component Extraction
|
||||
|
||||
The system SHALL not contain inner component definitions.
|
||||
|
||||
**Rationale:** Inner components cannot be tested independently, cause re-creation on every render, and hide complexity.
|
||||
|
||||
#### Scenario: No inner components in pages
|
||||
- **GIVEN** any file under `pages/`
|
||||
- **WHEN** searching for `const [A-Z]` followed by a component body
|
||||
- **THEN** zero matches are found
|
||||
- **AND** all previously inner components are moved to `components/`
|
||||
|
||||
### REQ-7: Reusable Loading/Error Patterns
|
||||
|
||||
The system SHALL extract repeated loading/error/retry UI into shared components.
|
||||
|
||||
**Rationale:** ~8 pages copy the same `status: "loading" | "ready" | "error"` pattern with identical retry buttons.
|
||||
|
||||
#### Scenario: Loading state
|
||||
- **GIVEN** a page is loading data
|
||||
- **WHEN** the UI renders
|
||||
- **THEN** it uses `<LoadingState message="Loading sessions..." />` instead of inline JSX
|
||||
|
||||
#### Scenario: Error state
|
||||
- **GIVEN** a page fails to load data
|
||||
- **WHEN** the UI renders
|
||||
- **THEN** it uses `<ErrorState message="Failed to load" onRetry={loadData} />` instead of inline JSX
|
||||
|
||||
### REQ-8: Barrel Exports at Stable Boundaries
|
||||
|
||||
The system SHALL provide `index.ts` barrel exports for stable module boundaries.
|
||||
|
||||
**Rationale:** Cleaner imports; encapsulates internal file structure.
|
||||
|
||||
#### Scenario: Importing UI primitives
|
||||
- **GIVEN** a developer needs `Button` and `Dialog`
|
||||
- **WHEN** they write the import
|
||||
- **THEN** they write `import { Button, Dialog } from "@/components/ui"`
|
||||
- **AND** not `import { Button } from "@/components/ui/button/button"`
|
||||
|
||||
#### Scenario: No barrel for utilities
|
||||
- **GIVEN** a developer needs `terminal-protocol` utilities
|
||||
- **WHEN** they write the import
|
||||
- **THEN** they write `import { encodeControlMessage } from "@/utils/terminal-protocol"`
|
||||
- **AND** `utils/index.ts` does not exist
|
||||
|
||||
---
|
||||
|
||||
## API / Protocol Changes
|
||||
|
||||
None. This is a pure reorganization refactor. All HTTP endpoints, WebSocket protocols, and database schemas remain unchanged.
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
No new dependencies required. Existing toolchain:
|
||||
- Frontend: React 18, Vite, TypeScript, ESLint, Vitest
|
||||
- Backend: FastAPI, SQLAlchemy, Alembic, pytest
|
||||
|
||||
**Optional consideration:** If CSS Modules are adopted (per proposal), Vite has built-in support — no new dependency needed.
|
||||
|
||||
---
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
- **Build time:** No regression in `npm run build` or `vite build` duration
|
||||
- **Bundle size:** No increase in output bundle size
|
||||
- **Test runtime:** No regression in `npm run test` or `pytest` duration
|
||||
- **Developer experience:** File discovery time (time to locate a component/service) must decrease
|
||||
|
||||
---
|
||||
|
||||
## Migration Order
|
||||
|
||||
| Phase | Concern | Files Touched | Est. Lines |
|
||||
|-------|---------|--------------|------------|
|
||||
| 1 | Types, seeds, shared deps | `types/`, `main.py`, `repo-workspace.tsx` | ~350 |
|
||||
| 2 | Style system | `styles.css` → `styles/` + `.module.css` | ~2,750 |
|
||||
| 3a | Backend shared deps | `auth/dependencies.py`, router helpers | ~400 |
|
||||
| 3b | `tool_instances` split | `api/tool_instances.py` → router + services | ~800 |
|
||||
| 3c | `git_repositories` + `config_profiles` split | Routers + services | ~700 |
|
||||
| 4a | `tool-workshop` split | Page + feature components | ~500 |
|
||||
| 4b | Pages split | `sessions.tsx`, `dashboard.tsx`, `repo-workspace.tsx` | ~600 |
|
||||
| 4c | Naming consistency | Rename files/components | ~350 |
|
||||
| 5 | Tests + docs | Backfill tests, naming doc | ~350 |
|
||||
|
||||
---
|
||||
|
||||
## Open Questions (Resolved)
|
||||
|
||||
| # | Question | Resolution |
|
||||
|---|----------|------------|
|
||||
| 1 | CSS approach | **CSS Modules** — each component gets its own `.module.css` |
|
||||
| 2 | Backend API versioning | **Keep flat `api/`** — version when v2 is actually needed |
|
||||
| 3 | Feature components location | **`components/features/{domain}/`** |
|
||||
| 4 | Barrel exports | **Yes for stable boundaries** (`components/ui/`, `components/features/{domain}/`, `types/`); **no for one-off utilities and API modules** |
|
||||
| 5 | Branching strategy | **Merge each phase to `main` immediately** |
|
||||
|
||||
---
|
||||
|
||||
*Spec prepared for SDD design phase. Next: technical design with exact file layout and import patterns.*
|
||||
@@ -0,0 +1,675 @@
|
||||
# Tasks: Repository Restructuring and Modularization
|
||||
|
||||
## Overview
|
||||
|
||||
9 reviewable PRs (all ≤ 400 lines changed) implementing the full restructure. Each task is a standalone merge to `main`. Dependencies are explicit. Review workload is protected.
|
||||
|
||||
**Conventions:**
|
||||
- `+N/-M` = lines added / removed in the PR
|
||||
- `Files: N` = number of files touched
|
||||
- `Deps:` = must-merge tasks before this one
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Safe Foundations
|
||||
|
||||
### Task 1.1: Centralize Types and Extract Seed Data
|
||||
**PR label:** `refactor: centralize types and extract seed data`
|
||||
**Estimated:** +180 / −120 lines across 15 files
|
||||
**Deps:** None
|
||||
|
||||
**What:**
|
||||
- Create `types/` directory with domain type files
|
||||
- Move types out of `api/sessions.ts`, `api/tool-types.ts`, `api/git-repositories.ts`, `api/config-folders.ts`
|
||||
- Move `Session` definition from `state/sessions.tsx` to `types/session.ts`
|
||||
- Move `ToolInstance` definition from `api/sessions.ts` to `types/tool-instance.ts`
|
||||
- Move hardcoded seed data from `main.py` to `seeds/builtin_tool_types.py`
|
||||
- Create `types/index.ts` barrel export
|
||||
- Update all consumers to import from `types/`
|
||||
|
||||
**Files:**
|
||||
```
|
||||
NEW: types/session.ts (from api/sessions.ts + state/sessions.tsx)
|
||||
NEW: types/tool-instance.ts (from api/sessions.ts)
|
||||
NEW: types/tool-type.ts (from api/tool-types.ts)
|
||||
NEW: types/git-repository.ts (from api/git-repositories.ts)
|
||||
NEW: types/config-folder.ts (from api/config-folders.ts)
|
||||
NEW: types/project.ts (from types.ts)
|
||||
NEW: types/user.ts (from types.ts)
|
||||
NEW: types/api-response.ts (new generic types)
|
||||
NEW: types/index.ts (barrel)
|
||||
NEW: seeds/builtin_tool_types.py (from main.py)
|
||||
MOD: api/sessions.ts (remove inline types, import from types/)
|
||||
MOD: api/tool-types.ts (remove inline types, import from types/)
|
||||
MOD: api/git-repositories.ts (remove inline types, import from types/)
|
||||
MOD: api/config-folders.ts (remove inline types, import from types/)
|
||||
MOD: state/sessions.tsx (import Session from types/)
|
||||
MOD: types.ts (remove moved types)
|
||||
MOD: main.py (import seed data from seeds/)
|
||||
```
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] `grep -n "interface Session" apps/web/src` returns exactly 1 result (in `types/session.ts`)
|
||||
- [ ] `grep -n "interface ToolInstance" apps/web/src` returns exactly 1 result
|
||||
- [ ] `tsc --noEmit` passes with zero errors
|
||||
- [ ] `pytest` passes
|
||||
- [ ] No behavior changes
|
||||
|
||||
---
|
||||
|
||||
### Task 1.2: Extract FileBrowser and Shared UI Components
|
||||
**PR label:** `refactor: extract FileBrowser and shared UI primitives`
|
||||
**Estimated:** +150 / −80 lines across 8 files
|
||||
**Deps:** 1.1
|
||||
|
||||
**What:**
|
||||
- Extract `FileBrowser` component from inline definition in `repo-workspace.tsx`
|
||||
- Create `components/features/git/FileBrowser.tsx`
|
||||
- Create `components/ui/LoadingState.tsx` (reusable loading pattern)
|
||||
- Create `components/ui/ErrorState.tsx` (reusable error+retry pattern)
|
||||
- Create `components/ui/index.ts` barrel
|
||||
- Update `repo-workspace.tsx` to import `FileBrowser`
|
||||
- Update pages that use loading/error patterns to use new components
|
||||
|
||||
**Files:**
|
||||
```
|
||||
NEW: components/features/git/FileBrowser.tsx (from repo-workspace.tsx)
|
||||
NEW: components/ui/LoadingState.tsx
|
||||
NEW: components/ui/ErrorState.tsx
|
||||
NEW: components/ui/StatusBadge.tsx
|
||||
NEW: components/ui/index.ts (barrel)
|
||||
MOD: pages/repo-workspace.tsx (remove inline FileBrowser, import)
|
||||
MOD: pages/dashboard.tsx (use LoadingState, ErrorState)
|
||||
MOD: pages/sessions.tsx (use LoadingState, ErrorState)
|
||||
```
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] `grep -n "const FileBrowser" pages/repo-workspace.tsx` returns zero results
|
||||
- [ ] FileBrowser renders correctly in repo workspace
|
||||
- [ ] `tsc --noEmit` passes
|
||||
- [ ] `eslint` passes
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Style System
|
||||
|
||||
### Task 2.1: Extract Global and Token Styles
|
||||
**PR label:** `refactor: split styles.css — global styles and tokens`
|
||||
**Estimated:** +120 / −50 lines across 5 files
|
||||
**Deps:** 1.2
|
||||
|
||||
**What:**
|
||||
- Create `styles/tokens.css` — CSS variables + dark theme
|
||||
- Create `styles/global.css` — reset, body, shell layout
|
||||
- Create `styles/utilities.css` — .stack, .row, .truncate, etc.
|
||||
- Create `styles/syntax-highlight.css` — Prism.js overrides
|
||||
- Update `main.tsx` to import the 4 new files
|
||||
- Do NOT delete `styles.css` yet
|
||||
|
||||
**Files:**
|
||||
```
|
||||
NEW: styles/tokens.css (from styles.css lines 1–80)
|
||||
NEW: styles/global.css (from styles.css: body, .shell, .shell-header, etc.)
|
||||
NEW: styles/utilities.css (from styles.css: .stack, .row, .truncate, etc.)
|
||||
NEW: styles/syntax-highlight.css (from styles.css: Prism overrides)
|
||||
MOD: main.tsx (add imports for new style files)
|
||||
```
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] All 4 new CSS files exist and contain only their concern
|
||||
- [ ] `npm run build` succeeds
|
||||
- [ ] No visual regressions on shell layout
|
||||
- [ ] `styles.css` still exists (deleted in Task 2.3)
|
||||
|
||||
---
|
||||
|
||||
### Task 2.2: Extract Component CSS Modules (Part 1 — Terminal + Git)
|
||||
**PR label:** `refactor: extract CSS modules for terminal and git components`
|
||||
**Estimated:** +280 / −200 lines across 14 files
|
||||
**Deps:** 2.1
|
||||
|
||||
**What:**
|
||||
- Create `.module.css` files for terminal and git components
|
||||
- Extract styles from `styles.css` for: Terminal, GitToolbar, FileBrowser, FileEditor, CommitPanel, CommitDialog, MergeDialog
|
||||
- Update components to import their `.module.css`
|
||||
- Convert global class names to camelCase module classes
|
||||
|
||||
**Files:**
|
||||
```
|
||||
NEW: components/features/terminal/TerminalComponent.module.css
|
||||
NEW: components/features/git/GitToolbar.module.css
|
||||
NEW: components/features/git/FileBrowser.module.css
|
||||
NEW: components/features/git/FileEditor.module.css
|
||||
NEW: components/features/git/CommitPanel.module.css
|
||||
NEW: components/features/git/CommitDialog.module.css
|
||||
NEW: components/features/git/MergeDialog.module.css
|
||||
MOD: components/terminal.tsx (import module, use styles.*)
|
||||
MOD: components/git-toolbar.tsx (import module, use styles.*)
|
||||
MOD: components/features/git/FileBrowser.tsx
|
||||
MOD: components/file-editor.tsx
|
||||
MOD: styles.css (remove extracted sections)
|
||||
```
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] Terminal renders identically
|
||||
- [ ] Git toolbar, file browser, file editor render identically
|
||||
- [ ] Commit panel and dialogs render identically
|
||||
- [ ] `npm run build` succeeds
|
||||
- [ ] `eslint` passes
|
||||
|
||||
---
|
||||
|
||||
### Task 2.3: Extract Component CSS Modules (Part 2 — Session + Settings + Layout) + Delete styles.css
|
||||
**PR label:** `refactor: extract CSS modules for session/settings + delete monolithic styles.css`
|
||||
**Estimated:** +250 / −2,500 lines across 12 files
|
||||
**Deps:** 2.2
|
||||
|
||||
**What:**
|
||||
- Create `.module.css` files for: InstanceList, AppShell, Navigation, SettingsTabLayout
|
||||
- Create `styles/pages/sessions.css`, `styles/pages/repo-workspace.css`, `styles/pages/tool-workshop.css`
|
||||
- Extract remaining component styles from `styles.css`
|
||||
- Update components to import modules
|
||||
- **Delete `styles.css`**
|
||||
- Verify no remaining references to `styles.css`
|
||||
|
||||
**Files:**
|
||||
```
|
||||
NEW: components/features/session/InstanceList.module.css
|
||||
NEW: components/layout/AppShell.module.css
|
||||
NEW: components/layout/Navigation.module.css
|
||||
NEW: components/features/settings/SettingsTabLayout.module.css
|
||||
NEW: styles/pages/sessions.css
|
||||
NEW: styles/pages/repo-workspace.css
|
||||
NEW: styles/pages/tool-workshop.css
|
||||
MOD: components/instance-list.tsx
|
||||
MOD: components/app-shell.tsx
|
||||
MOD: components/settings-tab-layout.tsx
|
||||
MOD: pages/sessions.tsx
|
||||
MOD: pages/repo-workspace.tsx
|
||||
DEL: styles.css
|
||||
```
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] `test -f styles.css` fails (file deleted)
|
||||
- [ ] All pages render identically
|
||||
- [ ] `npm run build` succeeds
|
||||
- [ ] No unstyled components
|
||||
- [ ] `eslint` passes
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Backend Decomposition
|
||||
|
||||
### Task 3.1: Extract Shared Auth Dependencies
|
||||
**PR label:** `refactor: extract shared auth dependencies`
|
||||
**Estimated:** +90 / −150 lines across 10 files
|
||||
**Deps:** 1.1
|
||||
|
||||
**What:**
|
||||
- Create `auth/dependencies.py` with `get_current_user()`, `get_owned_project()`, `get_owned_repository()`
|
||||
- Find and remove duplicated `_get_user()` / `_get_owned_project()` helpers from all routers
|
||||
- Update routers to import from `auth.dependencies`
|
||||
- Ensure dependency signatures match across all routers
|
||||
|
||||
**Files:**
|
||||
```
|
||||
NEW: auth/dependencies.py (consolidated from router files)
|
||||
MOD: api/tool_instances.py (remove inline helpers, import)
|
||||
MOD: api/git_repositories.py (remove inline helpers, import)
|
||||
MOD: api/config_profiles.py (remove inline helpers, import)
|
||||
MOD: api/ssh_keys.py (remove inline helpers, import)
|
||||
MOD: api/projects.py (remove inline helpers, import)
|
||||
MOD: api/tool_configs.py (remove inline helpers, import)
|
||||
MOD: api/config_folders.py (remove inline helpers, import)
|
||||
MOD: api/terminal.py (remove inline helpers, import)
|
||||
```
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] `grep -rn "def _get_user" apps/api/src/api/` returns zero results
|
||||
- [ ] `grep -rn "def _get_owned_project" apps/api/src/api/` returns zero results
|
||||
- [ ] All integration tests pass
|
||||
- [ ] `pytest` passes
|
||||
|
||||
---
|
||||
|
||||
### Task 3.2: Create Pydantic Schemas Directory
|
||||
**PR label:** `refactor: extract pydantic schemas from routers`
|
||||
**Estimated:** +200 / −100 lines across 8 files
|
||||
**Deps:** 3.1
|
||||
|
||||
**What:**
|
||||
- Create `schemas/` directory
|
||||
- Extract request/response models from `api/tool_instances.py` → `schemas/tool_instance.py`
|
||||
- Extract from `api/git_repositories.py` → `schemas/git_repository.py`
|
||||
- Extract from `api/config_profiles.py` → `schemas/config_profile.py`
|
||||
- Extract from `api/tool_types.py` → `schemas/tool_type.py`
|
||||
- Update routers to import schemas
|
||||
- Keep schema imports backward-compatible (routers still work)
|
||||
|
||||
**Files:**
|
||||
```
|
||||
NEW: schemas/tool_instance.py
|
||||
NEW: schemas/git_repository.py
|
||||
NEW: schemas/config_profile.py
|
||||
NEW: schemas/tool_type.py
|
||||
NEW: schemas/ssh_key.py
|
||||
NEW: schemas/project.py
|
||||
MOD: api/tool_instances.py (remove inline schemas, import)
|
||||
MOD: api/git_repositories.py (remove inline schemas, import)
|
||||
MOD: api/config_profiles.py (remove inline schemas, import)
|
||||
MOD: api/tool_types.py (remove inline schemas, import)
|
||||
```
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] No Pydantic `BaseModel` definitions in router files
|
||||
- [ ] `pytest` passes
|
||||
- [ ] All API endpoints return correct response shapes
|
||||
|
||||
---
|
||||
|
||||
### Task 3.3: Split services/docker.py into Focused Modules
|
||||
**PR label:** `refactor: split services/docker.py into focused modules`
|
||||
**Estimated:** +350 / −300 lines across 6 files
|
||||
**Deps:** 3.2
|
||||
|
||||
**What:**
|
||||
- Create `services/docker/compose.py` — compose file generation + modification
|
||||
- Create `services/docker/container.py` — container lifecycle (create, start, stop, restart, remove)
|
||||
- Create `services/docker/tunnel.py` — Cloudflare tunnel create/recreate/health
|
||||
- Create `services/docker/config_staging.py` — config folder file writing
|
||||
- Create `services/docker/__init__.py` barrel
|
||||
- Delete `services/docker.py`
|
||||
- Update `api/tool_instances.py` to import from `services.docker`
|
||||
|
||||
**Files:**
|
||||
```
|
||||
NEW: services/docker/__init__.py
|
||||
NEW: services/docker/compose.py
|
||||
NEW: services/docker/container.py
|
||||
NEW: services/docker/tunnel.py
|
||||
NEW: services/docker/config_staging.py
|
||||
MOD: api/tool_instances.py (update imports)
|
||||
DEL: services/docker.py
|
||||
```
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] `test -f services/docker.py` fails (deleted)
|
||||
- [ ] Each new module ≤ 300 lines
|
||||
- [ ] `pytest` passes
|
||||
- [ ] Tool instance create/start/stop/restart still works
|
||||
|
||||
---
|
||||
|
||||
### Task 3.4: Slim tool_instances.py Router
|
||||
**PR label:** `refactor: slim tool_instances router to HTTP-only concerns`
|
||||
**Estimated:** +80 / −700 lines across 3 files
|
||||
**Deps:** 3.3
|
||||
|
||||
**What:**
|
||||
- Remove all business logic from `api/tool_instances.py`
|
||||
- Move compose generation calls to `services.docker.compose`
|
||||
- Move container lifecycle calls to `services.docker.container`
|
||||
- Move tunnel calls to `services.docker.tunnel`
|
||||
- Move config staging calls to `services.docker.config_staging`
|
||||
- Router should only: validate input, call service, return response
|
||||
- Target: ~250 lines
|
||||
|
||||
**Files:**
|
||||
```
|
||||
MOD: api/tool_instances.py (remove ~700 lines of logic, keep ~250 of routing)
|
||||
MOD: services/docker/compose.py (may need minor adjustments)
|
||||
MOD: services/docker/container.py (may need minor adjustments)
|
||||
```
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] `api/tool_instances.py` ≤ 300 lines
|
||||
- [ ] `grep -n "subprocess" api/tool_instances.py` returns zero results
|
||||
- [ ] `grep -n "docker" api/tool_instances.py` returns only import lines
|
||||
- [ ] `pytest` passes, especially tool instance integration tests
|
||||
|
||||
---
|
||||
|
||||
### Task 3.5: Slim git_repositories.py and config_profiles.py Routers
|
||||
**PR label:** `refactor: slim git_repositories and config_profiles routers`
|
||||
**Estimated:** +100 / −600 lines across 6 files
|
||||
**Deps:** 3.4
|
||||
|
||||
**What:**
|
||||
- Extract git control logic from `api/git_repositories.py` to `services/git/control.py` (already exists, use more)
|
||||
- Extract file browsing logic to thin handlers delegating to `services/git/files.py`
|
||||
- Extract config profile resolution logic to `services/profile_resolver.py`
|
||||
- Slim both routers to ~250 lines each
|
||||
- Ensure routers contain only route definitions and thin handlers
|
||||
|
||||
**Files:**
|
||||
```
|
||||
MOD: api/git_repositories.py (remove business logic, delegate)
|
||||
MOD: api/config_profiles.py (remove business logic, delegate)
|
||||
MOD: services/git/control.py (may expand)
|
||||
MOD: services/git/files.py (may expand)
|
||||
MOD: services/profile_resolver.py (may expand)
|
||||
```
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] `api/git_repositories.py` ≤ 300 lines
|
||||
- [ ] `api/config_profiles.py` ≤ 300 lines
|
||||
- [ ] `pytest` passes
|
||||
- [ ] Git operations (branch, commit, push, pull) still work
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Frontend Page Decomposition
|
||||
|
||||
### Task 4.1: Split tool-workshop.tsx into Tab Components
|
||||
**PR label:** `refactor: split tool-workshop page into tab components`
|
||||
**Estimated:** +280 / −450 lines across 6 files
|
||||
**Deps:** 2.3
|
||||
|
||||
**What:**
|
||||
- Create `components/features/tool-workshop/ToolTypesTab.tsx`
|
||||
- Create `components/features/tool-workshop/ToolConfigsTab.tsx`
|
||||
- Create `components/features/tool-workshop/ConfigFoldersTab.tsx`
|
||||
- Create `components/features/tool-workshop/index.ts` barrel
|
||||
- Slim `pages/tool-workshop.tsx` to tab switcher + layout only (~100 lines)
|
||||
- Each tab manages its own form state and API calls
|
||||
|
||||
**Files:**
|
||||
```
|
||||
NEW: components/features/tool-workshop/ToolTypesTab.tsx
|
||||
NEW: components/features/tool-workshop/ToolConfigsTab.tsx
|
||||
NEW: components/features/tool-workshop/ConfigFoldersTab.tsx
|
||||
NEW: components/features/tool-workshop/index.ts
|
||||
MOD: pages/tool-workshop.tsx (remove inline tabs, compose imports)
|
||||
```
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] `pages/tool-workshop.tsx` ≤ 150 lines
|
||||
- [ ] All 3 tabs function identically
|
||||
- [ ] `tsc --noEmit` passes
|
||||
- [ ] `eslint` passes
|
||||
|
||||
---
|
||||
|
||||
### Task 4.2: Extract SessionsPage Components
|
||||
**PR label:** `refactor: extract sessions page components`
|
||||
**Estimated:** +220 / −350 lines across 7 files
|
||||
**Deps:** 4.1
|
||||
|
||||
**What:**
|
||||
- Create `components/features/session/SessionList.tsx`
|
||||
- Create `components/features/session/SessionCard.tsx`
|
||||
- Create `components/features/session/CreateSessionForm.tsx`
|
||||
- Create `components/features/session/index.ts` barrel
|
||||
- Slim `pages/sessions.tsx` to layout + composition
|
||||
- Extract inline stop/delete confirmation into reusable `ConfirmDialog` in `components/ui/`
|
||||
|
||||
**Files:**
|
||||
```
|
||||
NEW: components/features/session/SessionList.tsx
|
||||
NEW: components/features/session/SessionCard.tsx
|
||||
NEW: components/features/session/CreateSessionForm.tsx
|
||||
NEW: components/features/session/index.ts
|
||||
NEW: components/ui/ConfirmDialog.tsx
|
||||
MOD: pages/sessions.tsx (remove inline lists/forms, compose)
|
||||
```
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] `pages/sessions.tsx` ≤ 200 lines
|
||||
- [ ] Session list, create form, and cards work identically
|
||||
- [ ] `tsc --noEmit` passes
|
||||
|
||||
---
|
||||
|
||||
### Task 4.3: Extract Dashboard and RepoWorkspace Components
|
||||
**PR label:** `refactor: extract dashboard and repo-workspace components`
|
||||
**Estimated:** +200 / −300 lines across 8 files
|
||||
**Deps:** 4.2
|
||||
|
||||
**What:**
|
||||
- Create `components/features/dashboard/DashboardSummary.tsx`
|
||||
- Create `components/features/dashboard/QuickActions.tsx`
|
||||
- Create `components/features/dashboard/ActiveSessionsList.tsx`
|
||||
- Create `components/features/dashboard/index.ts` barrel
|
||||
- Slim `pages/dashboard.tsx` to layout + composition
|
||||
- Slim `pages/repo-workspace.tsx` further (FileBrowser already extracted in 1.2)
|
||||
- Extract `InstanceList` inline create dialog to `components/features/session/CreateInstanceDialog.tsx`
|
||||
|
||||
**Files:**
|
||||
```
|
||||
NEW: components/features/dashboard/DashboardSummary.tsx
|
||||
NEW: components/features/dashboard/QuickActions.tsx
|
||||
NEW: components/features/dashboard/ActiveSessionsList.tsx
|
||||
NEW: components/features/dashboard/index.ts
|
||||
NEW: components/features/session/CreateInstanceDialog.tsx
|
||||
MOD: pages/dashboard.tsx (slim to ~120 lines)
|
||||
MOD: pages/repo-workspace.tsx (slim further)
|
||||
MOD: components/instance-list.tsx (extract create dialog)
|
||||
```
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] `pages/dashboard.tsx` ≤ 150 lines
|
||||
- [ ] Dashboard renders identically
|
||||
- [ ] `tsc --noEmit` passes
|
||||
|
||||
---
|
||||
|
||||
### Task 4.4: Rename All Files to Naming Convention
|
||||
**PR label:** `refactor: rename files to PascalCase components and kebab-case APIs`
|
||||
**Estimated:** +30 / −0 lines across 40 files (mostly `git mv`)
|
||||
**Deps:** 4.3
|
||||
|
||||
**What:**
|
||||
- Rename component files to PascalCase matching exported name:
|
||||
- `app-shell.tsx` → `AppShell.tsx`
|
||||
- `git-toolbar.tsx` → `GitToolbar.tsx`
|
||||
- `file-editor.tsx` → `FileEditor.tsx`
|
||||
- `instance-list.tsx` → `InstanceList.tsx`
|
||||
- `terminal.tsx` → `TerminalComponent.tsx`
|
||||
- etc.
|
||||
- Rename page files to PascalCase:
|
||||
- `dashboard.tsx` → `DashboardPage.tsx`
|
||||
- `git-history.tsx` → `GitHistoryPage.tsx`
|
||||
- `repo-workspace.tsx` → `RepoWorkspacePage.tsx`
|
||||
- etc.
|
||||
- Rename API files to kebab-case:
|
||||
- `tool_types.ts` → `tool-types.ts`
|
||||
- `git_repositories.ts` → `git-repositories.ts`
|
||||
- `config_folders.ts` → `config-folders.ts`
|
||||
- etc.
|
||||
- Update `router.tsx` to import new page paths
|
||||
- Update all imports across the codebase
|
||||
|
||||
**Files:**
|
||||
```
|
||||
# Component renames (git mv)
|
||||
components/app-shell.tsx → components/layout/AppShell.tsx
|
||||
components/git-toolbar.tsx → components/features/git/GitToolbar.tsx
|
||||
components/file-editor.tsx → components/features/git/FileEditor.tsx
|
||||
components/instance-list.tsx → components/features/session/InstanceList.tsx
|
||||
components/terminal.tsx → components/features/terminal/TerminalComponent.tsx
|
||||
components/code-editor.tsx → components/ui/CodeEditor.tsx
|
||||
components/commit-dialog.tsx → components/features/git/CommitDialog.tsx
|
||||
components/commit-panel.tsx → components/features/git/CommitPanel.tsx
|
||||
components/merge-dialog.tsx → components/features/git/MergeDialog.tsx
|
||||
components/protected-route.tsx → components/ProtectedRoute.tsx
|
||||
components/repositories-settings-tab.tsx → components/features/project/RepositoriesSettingsTab.tsx
|
||||
components/repository-create-dialog.tsx → components/features/project/RepositoryCreateDialog.tsx
|
||||
components/settings-tab-layout.tsx → components/features/settings/SettingsTabLayout.tsx
|
||||
components/syntax-highlighter.tsx → components/features/git/SyntaxHighlighter.tsx
|
||||
components/workspace-header.tsx → components/features/workspace/WorkspaceHeader.tsx
|
||||
components/icon.tsx → components/ui/Icon.tsx
|
||||
|
||||
# Page renames (git mv)
|
||||
pages/dashboard.tsx → pages/DashboardPage.tsx
|
||||
pages/git-history.tsx → pages/GitHistoryPage.tsx
|
||||
pages/git-repositories.tsx → pages/GitRepositoriesPage.tsx
|
||||
pages/profile.tsx → pages/ProfilePage.tsx
|
||||
pages/project-settings.tsx → pages/ProjectSettingsPage.tsx
|
||||
pages/projects.tsx → pages/ProjectsPage.tsx
|
||||
pages/repo-workspace.tsx → pages/RepoWorkspacePage.tsx
|
||||
pages/sessions.tsx → pages/SessionsPage.tsx
|
||||
pages/settings.tsx → pages/SettingsPage.tsx
|
||||
pages/ssh-keys.tsx → pages/SshKeysPage.tsx
|
||||
pages/terminal.tsx → pages/TerminalPage.tsx
|
||||
pages/tool-configs.tsx → pages/ToolConfigsPage.tsx
|
||||
pages/tool-types.tsx → pages/ToolTypesPage.tsx
|
||||
pages/tool-workshop.tsx → pages/ToolWorkshopPage.tsx
|
||||
pages/placeholder.tsx → pages/PlaceholderPage.tsx
|
||||
|
||||
# API renames (git mv)
|
||||
api/tool_types.ts → api/tool-types.ts
|
||||
api/git_repositories.ts → api/git-repositories.ts
|
||||
api/config_folders.ts → api/config-folders.ts
|
||||
api/tool_configs.ts → api/tool-configs.ts
|
||||
api/ssh_keys.ts → api/ssh-keys.ts
|
||||
api/user_config.ts → api/user-config.ts
|
||||
|
||||
# Updated imports
|
||||
MOD: router.tsx
|
||||
MOD: all page files (update relative imports)
|
||||
MOD: all component files (update relative imports)
|
||||
MOD: all test files (update imports)
|
||||
```
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] All component files match exported component name (case-insensitive)
|
||||
- [ ] All page files end with `Page.tsx`
|
||||
- [ ] All API files use kebab-case
|
||||
- [ ] `tsc --noEmit` passes
|
||||
- [ ] `eslint` passes
|
||||
- [ ] `vitest run` passes
|
||||
- [ ] Router resolves all routes
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Testing and Polish
|
||||
|
||||
### Task 5.1: Add Tests for Extracted Components
|
||||
**PR label:** `test: add tests for extracted components`
|
||||
**Estimated:** +250 / −0 lines across 8 files
|
||||
**Deps:** 4.4
|
||||
|
||||
**What:**
|
||||
- Add `components/features/git/FileBrowser.test.tsx`
|
||||
- Add `components/ui/LoadingState.test.tsx`
|
||||
- Add `components/ui/ErrorState.test.tsx`
|
||||
- Add `components/features/tool-workshop/ToolTypesTab.test.tsx`
|
||||
- Add `components/features/session/SessionList.test.tsx`
|
||||
- Add `pages/DashboardPage.test.tsx` (replace failing `projects.test.tsx` pattern)
|
||||
- Ensure tests use `MemoryRouter` where needed
|
||||
- Mock API calls consistently
|
||||
|
||||
**Files:**
|
||||
```
|
||||
NEW: components/features/git/FileBrowser.test.tsx
|
||||
NEW: components/ui/LoadingState.test.tsx
|
||||
NEW: components/ui/ErrorState.test.tsx
|
||||
NEW: components/features/tool-workshop/ToolTypesTab.test.tsx
|
||||
NEW: components/features/session/SessionList.test.tsx
|
||||
NEW: pages/DashboardPage.test.tsx
|
||||
```
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] All new tests pass (`vitest run`)
|
||||
- [ ] No test file exceeds 200 lines
|
||||
- [ ] Tests cover render, basic interaction, and error states
|
||||
|
||||
---
|
||||
|
||||
### Task 5.2: Documentation and Cleanup
|
||||
**PR label:** `docs: add naming conventions doc and final cleanup`
|
||||
**Estimated:** +120 / −50 lines across 6 files
|
||||
**Deps:** 5.1
|
||||
|
||||
**What:**
|
||||
- Write `docs/development/naming.md` with full naming convention table
|
||||
- Remove dead CSS classes (verified by grep for unused selectors)
|
||||
- Remove unused exports (check `eslint` `report-unused-disable-directives`)
|
||||
- Add verification script to `package.json`: `"check-structure": "node scripts/check-structure.js"`
|
||||
- Final quality gate run
|
||||
|
||||
**Files:**
|
||||
```
|
||||
NEW: docs/development/naming.md
|
||||
NEW: scripts/check-structure.js (verifies file sizes, naming, barrels)
|
||||
MOD: package.json (add check-structure script)
|
||||
MOD: styles/global.css (remove dead rules if any)
|
||||
MOD: various files (remove unused exports)
|
||||
```
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] `docs/development/naming.md` exists and is complete
|
||||
- [ ] `npm run check-structure` passes
|
||||
- [ ] No file in `src/` exceeds 300 lines
|
||||
- [ ] `tsc --noEmit` passes
|
||||
- [ ] `eslint` passes
|
||||
- [ ] `vitest run` passes
|
||||
- [ ] `pytest` passes
|
||||
|
||||
---
|
||||
|
||||
## Task Dependency Graph
|
||||
|
||||
```
|
||||
1.1 (Types + Seeds) ──┐
|
||||
├──→ 1.2 (FileBrowser + UI) ──→ 2.1 (Global Styles)
|
||||
│
|
||||
3.1 (Auth deps) ──→ 3.2 (Schemas) ─┤
|
||||
│ │
|
||||
└──→ 3.3 (Docker split) ──→ 3.4 (tool_instances slim)
|
||||
│
|
||||
└──→ 3.5 (git + profiles slim)
|
||||
│
|
||||
2.2 (Terminal/Git CSS) ──→ 2.3 (Session/Settings CSS + delete styles.css) ────────────────┘ │
|
||||
│
|
||||
4.1 (tool-workshop split) ──→ 4.2 (sessions split) ──→ 4.3 (dashboard/workspace split) ──→ 4.4 (rename files)
|
||||
│
|
||||
5.1 (tests) ──→ 5.2 (docs + cleanup) ────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Workload Summary
|
||||
|
||||
| Task | Est. Lines | Status |
|
||||
|------|-----------|--------|
|
||||
| 1.1 | +180 / −120 | ✅ Under 400 |
|
||||
| 1.2 | +150 / −80 | ✅ Under 400 |
|
||||
| 2.1 | +120 / −50 | ✅ Under 400 |
|
||||
| 2.2 | +280 / −200 | ✅ Under 400 |
|
||||
| 2.3 | +250 / −2,500 | ✅ Under 400 (mostly deletions) |
|
||||
| 3.1 | +90 / −150 | ✅ Under 400 |
|
||||
| 3.2 | +200 / −100 | ✅ Under 400 |
|
||||
| 3.3 | +350 / −300 | ✅ Under 400 |
|
||||
| 3.4 | +80 / −700 | ✅ Under 400 |
|
||||
| 3.5 | +100 / −600 | ✅ Under 400 |
|
||||
| 4.1 | +280 / −450 | ✅ Under 400 |
|
||||
| 4.2 | +220 / −350 | ✅ Under 400 |
|
||||
| 4.3 | +200 / −300 | ✅ Under 400 |
|
||||
| 4.4 | +30 / −0 | ✅ Under 400 (git mv mostly) |
|
||||
| 5.1 | +250 / −0 | ✅ Under 400 |
|
||||
| 5.2 | +120 / −50 | ✅ Under 400 |
|
||||
|
||||
**All 16 tasks are under the 400-line review budget.**
|
||||
|
||||
---
|
||||
|
||||
## Quality Gates (Per Task)
|
||||
|
||||
Every task MUST pass:
|
||||
1. `npm run typecheck` (frontend) — zero errors
|
||||
2. `npm run lint` (frontend) — zero warnings
|
||||
3. `pytest` (backend) — zero failures
|
||||
4. File size check — no file > 300 lines
|
||||
5. For frontend tasks: visual sanity check (build succeeds)
|
||||
6. Commit with conventional format: `refactor: phase N — description`
|
||||
|
||||
---
|
||||
|
||||
## Task Execution Notes
|
||||
|
||||
- **Use `git mv`** for all file renames to preserve history
|
||||
- **Update imports with IDE refactor** when possible (VS Code "Move to new file", PyCharm refactor)
|
||||
- **No logic changes** — pure cut-paste-reorganize
|
||||
- **Merge to `main` immediately** after each task passes quality gates
|
||||
- **Pause between phases** (after Tasks 1.2, 2.3, 3.5, 4.4) to verify stability
|
||||
Reference in New Issue
Block a user