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:
Developer
2026-06-02 18:56:54 +00:00
parent d894cd9723
commit ee1fa6bee5
36 changed files with 3003 additions and 435 deletions
+1 -152
View File
@@ -7,8 +7,6 @@ from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from sqlalchemy import select, text
from src.api.auth import router as auth_router from src.api.auth import router as auth_router
from src.api.dashboard import router as dashboard_router from src.api.dashboard import router as dashboard_router
from src.api.git_repositories import router as git_repositories_router from src.api.git_repositories import router as git_repositories_router
@@ -32,7 +30,7 @@ from src.logging_config import (
RequestLoggingMiddleware, RequestLoggingMiddleware,
configure_logging, configure_logging,
) )
from src.models.tool_type import ToolType from src.seeds.builtin_tool_types import seed_builtin_tool_types
# Configure logging early # Configure logging early
log_level = os.getenv("LOG_LEVEL", "INFO").upper() 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") @app.on_event("startup")
async def on_startup(): async def on_startup():
logger.info("Starting up Headquarter API...") logger.info("Starting up Headquarter API...")
View File
+161
View File
@@ -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.")
+16 -34
View File
@@ -1,38 +1,17 @@
import { apiClient } from "./client"; import { apiClient } from "./client";
import type {
ConfigFolder,
CreateConfigFolderRequest,
UpdateConfigFolderRequest,
ProjectOverrideRequest,
} from "../types/config-folder";
export interface ConfigFolder { export type {
id: string; ConfigFolder,
user_id: string; CreateConfigFolderRequest,
name: string; UpdateConfigFolderRequest,
description: string | null; ProjectOverrideRequest,
mount_path: string; } from "../types/config-folder";
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 const listConfigFolders = async (): Promise<ConfigFolder[]> => { export const listConfigFolders = async (): Promise<ConfigFolder[]> => {
const response = await apiClient.get<ConfigFolder[]>("/config-folders"); const response = await apiClient.get<ConfigFolder[]>("/config-folders");
@@ -55,7 +34,10 @@ export const updateConfigFolder = async (
id: string, id: string,
data: UpdateConfigFolderRequest data: UpdateConfigFolderRequest
): Promise<ConfigFolder> => { ): 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; return response.data;
}; };
+21 -87
View File
@@ -1,32 +1,26 @@
import { apiClient } from "./client"; import { apiClient } from "./client";
import type {
CommitDetail,
CommitHistoryResponse,
CommitResponse,
GitRepository,
GitRepositoryCreate,
GitStatus,
MergeResponse,
URLParseResult,
} from "../types/git-repository";
export interface GitRepository { export type {
id: string; CommitDetail,
name: string; CommitHistoryEntry,
path: string; CommitHistoryResponse,
project_id: string; CommitResponse,
owner_id: string; GitRepository,
is_mirror: boolean; GitRepositoryCreate,
remote_url: string | null; GitStatus,
last_push: string | null; MergeResponse,
created_at: string | null; URLParseResult,
} } from "../types/git-repository";
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 async function parseGitUrl(url: string): Promise<URLParseResult> { export async function parseGitUrl(url: string): Promise<URLParseResult> {
const response = await apiClient.post("/projects/repositories/parse-url", { url }); 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}`); 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( export async function getRepositoryHistory(
projectId: string, projectId: string,
repoId: string, repoId: string,
@@ -83,25 +59,6 @@ export async function getRepositoryHistory(
return response.data; 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( export async function getCommitDetail(
projectId: string, projectId: string,
repoId: string, repoId: string,
@@ -113,19 +70,6 @@ export async function getCommitDetail(
return response.data; 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( export async function getRepositoryStatus(
projectId: string, projectId: string,
repoId: string repoId: string
@@ -173,11 +117,6 @@ export async function checkoutBranch(
return response.data; return response.data;
} }
export interface CommitResponse {
commit_hash: string;
message: string;
}
export async function commitChanges( export async function commitChanges(
projectId: string, projectId: string,
repoId: string, repoId: string,
@@ -225,11 +164,6 @@ export async function pushRepository(
return response.data; return response.data;
} }
export interface MergeResponse {
commit_hash: string;
message: string;
}
export async function mergeBranches( export async function mergeBranches(
projectId: string, projectId: string,
repoId: string, repoId: string,
+4 -26
View File
@@ -1,31 +1,9 @@
import { apiClient } from "./client"; import { apiClient } from "./client";
import type { Session } from "../types/session";
import type { ToolInstance } from "../types/tool-instance";
export interface ToolInstance { export type { Session } from "../types/session";
id: string; export type { ToolInstance } from "../types/tool-instance";
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 async function listInstances( export async function listInstances(
projectId: string, projectId: string,
+2 -28
View File
@@ -1,33 +1,7 @@
import { apiClient } from "./client"; import { apiClient } from "./client";
import type { ToolConfig, CreateToolConfigRequest } from "../types/tool-config";
export interface ToolConfig { export type { ToolConfig, CreateToolConfigRequest } from "../types/tool-config";
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 const listToolConfigs = async ( export const listToolConfigs = async (
tool_type_id?: string, tool_type_id?: string,
+2 -54
View File
@@ -1,59 +1,7 @@
import { apiClient } from "./client"; import { apiClient } from "./client";
import type { ToolType, CreateToolTypeRequest, UpdateToolTypeRequest } from "../types/tool-type";
export interface ReadinessProbe { export type { ReadinessProbe, ToolType, CreateToolTypeRequest, UpdateToolTypeRequest } from "../types/tool-type";
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 const listToolTypes = async (): Promise<ToolType[]> => { export const listToolTypes = async (): Promise<ToolType[]> => {
const response = await apiClient.get<ToolType[]>("/tool-types"); const response = await apiClient.get<ToolType[]>("/tool-types");
+1 -1
View File
@@ -2,7 +2,7 @@ import { useCallback, useEffect } from "react";
import { Link, NavLink, Outlet } from "react-router-dom"; import { Link, NavLink, Outlet } from "react-router-dom";
import { getUserSessions } from "../api/sessions"; import { getUserSessions } from "../api/sessions";
import type { Session } from "../api/sessions"; import type { Session } from "../types/session";
import { useTheme } from "../hooks/use-theme"; import { useTheme } from "../hooks/use-theme";
import { useAuth } from "../state/auth"; import { useAuth } from "../state/auth";
import { useSessions } from "../state/sessions"; import { useSessions } from "../state/sessions";
+2 -2
View File
@@ -1,7 +1,8 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Icon } from "./icon"; 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 { import {
checkInstanceHealth, checkInstanceHealth,
createInstance, createInstance,
@@ -12,7 +13,6 @@ import {
startInstance, startInstance,
stopInstance, stopInstance,
} from "../api/sessions"; } from "../api/sessions";
import type { ToolType } from "../api/tool_types";
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000"; 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 React, { useCallback, useEffect, useState } from "react";
import { useParams } from "react-router-dom"; 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 { RepositoryCreateDialog } from "./repository-create-dialog";
import { Icon } from "./icon"; import { Icon } from "./icon";
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from "react"; 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"; import { Icon } from "./icon";
type CreateMode = "clone" | "blank"; type CreateMode = "clone" | "blank";
+7 -4
View File
@@ -2,12 +2,15 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { getDashboardSummary, type DashboardSummary } from "../api/dashboard"; 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 { listProjects } from "../api/projects";
import { listRepositories, type GitRepository } from "../api/git_repositories"; import { listRepositories } from "../api/git_repositories";
import { listToolTypes, type ToolType } from "../api/tool_types"; import { listToolTypes } from "../api/tool_types";
import { updateUserConfig } from "../api/settings"; 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"; import { Icon } from "../components/icon";
type HomeStatus = "loading" | "ready" | "error"; type HomeStatus = "loading" | "ready" | "error";
+1 -1
View File
@@ -1,11 +1,11 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import type { GitRepository } from "../types/git-repository";
import { import {
deleteRepository, deleteRepository,
listRepositories, listRepositories,
} from "../api/git_repositories"; } from "../api/git_repositories";
import type { GitRepository } from "../api/git_repositories";
import { Icon } from "../components/icon"; import { Icon } from "../components/icon";
import { RepositoryCreateDialog } from "../components/repository-create-dialog"; import { RepositoryCreateDialog } from "../components/repository-create-dialog";
+2 -2
View File
@@ -4,10 +4,11 @@ import { Icon } from "../components/icon";
import { Link, useParams, useSearchParams } from "react-router-dom"; import { Link, useParams, useSearchParams } from "react-router-dom";
import { apiClient } from "../api/client"; import { apiClient } from "../api/client";
import type { GitRepository } from "../types/git-repository";
import type { ToolType } from "../types/tool-type";
import { import {
getRepositoryStatus, getRepositoryStatus,
listRepositories, listRepositories,
type GitRepository,
type GitStatus, type GitStatus,
} from "../api/git_repositories"; } from "../api/git_repositories";
import { CommitPanel } from "../components/commit-panel"; import { CommitPanel } from "../components/commit-panel";
@@ -16,7 +17,6 @@ import { GitToolbar } from "../components/git-toolbar";
import { InstanceList } from "../components/instance-list"; import { InstanceList } from "../components/instance-list";
import { WorkspaceHeader } from "../components/workspace-header"; import { WorkspaceHeader } from "../components/workspace-header";
import { listToolTypes } from "../api/tool_types"; import { listToolTypes } from "../api/tool_types";
import type { ToolType } from "../api/tool_types";
type WorkspaceStatus = "loading" | "ready" | "error" | "empty"; type WorkspaceStatus = "loading" | "ready" | "error" | "empty";
+7 -5
View File
@@ -2,20 +2,22 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { listProjects } from "../api/projects"; import { listProjects } from "../api/projects";
import type { Project } from "../types"; import { listRepositories } from "../api/git_repositories";
import { listRepositories, type GitRepository } from "../api/git_repositories";
import { import {
getUserSessions, getUserSessions,
type Session,
deleteInstance, deleteInstance,
stopInstance, stopInstance,
startInstance, startInstance,
checkInstanceHealth, checkInstanceHealth,
recreateInstanceTunnel, recreateInstanceTunnel,
createInstance,
} from "../api/sessions"; } from "../api/sessions";
import { listToolTypes, type ToolType } from "../api/tool_types"; import { listToolTypes } from "../api/tool_types";
import { createInstance } from "../api/sessions";
import { getUserConfig, updateUserConfig } from "../api/settings"; 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"; import { Icon } from "../components/icon";
type SessionsStatus = "loading" | "ready" | "error"; type SessionsStatus = "loading" | "ready" | "error";
+3 -2
View File
@@ -2,13 +2,14 @@ import { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Icon } from "../components/icon"; 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 { import {
createToolConfig, createToolConfig,
deleteToolConfig, deleteToolConfig,
listToolConfigs, listToolConfigs,
updateToolConfig, updateToolConfig,
type ToolConfig,
} from "../api/tool_configs"; } from "../api/tool_configs";
type ConfigStatus = "loading" | "ready" | "error"; type ConfigStatus = "loading" | "ready" | "error";
+1 -3
View File
@@ -1,16 +1,14 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import type { ToolType, CreateToolTypeRequest, UpdateToolTypeRequest } from "../types/tool-type";
import { import {
createToolType, createToolType,
deleteToolType, deleteToolType,
listToolTypes, listToolTypes,
updateToolType, updateToolType,
type CreateToolTypeRequest,
type UpdateToolTypeRequest,
} from "../api/tool_types"; } from "../api/tool_types";
import { Icon } from "../components/icon"; import { Icon } from "../components/icon";
import type { ToolType } from "../api/tool_types";
type ToolTypesStatus = "loading" | "ready" | "error"; type ToolTypesStatus = "loading" | "ready" | "error";
type DialogMode = "none" | "create" | "edit"; type DialogMode = "none" | "create" | "edit";
+2 -13
View File
@@ -1,18 +1,7 @@
import { createContext, useCallback, useContext, useState, type ReactNode } from "react"; import { createContext, useCallback, useContext, useState, type ReactNode } from "react";
import type { Session } from "../types/session";
export interface Session { export type { Session } from "../types/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;
}
interface SessionsContextType { interface SessionsContextType {
sessions: Session[]; sessions: Session[];
+4 -17
View File
@@ -1,18 +1,5 @@
export type SessionUser = { // Legacy types file — being migrated to types/ directory.
id: string; // This file will be removed once all consumers are updated.
email: string;
name: string;
avatar_url: string | null;
};
export type SessionPayload = { export type { Project } from "./types/project";
user: SessionUser; export type { SessionPayload, SessionUser } from "./types/user";
};
export type Project = {
id: string;
name: string;
description: string | null;
owner_id: string;
default_ssh_key_id: string | null;
};
+10
View File
@@ -0,0 +1,10 @@
export interface ApiResponse<T> {
data: T;
}
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
page_size: number;
}
+33
View File
@@ -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>;
}
+85
View File
@@ -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;
}
+24
View File
@@ -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";
+7
View File
@@ -0,0 +1,7 @@
export type Project = {
id: string;
name: string;
description: string | null;
owner_id: string;
default_ssh_key_id: string | null;
};
+13
View File
@@ -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;
}
+28
View File
@@ -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 }>;
}
+12
View File
@@ -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;
}
+54
View File
@@ -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[];
}
+10
View File
@@ -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.
+723
View File
@@ -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.*
+329
View File
@@ -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: 100200). 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:** 100200 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.*
+675
View File
@@ -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 180)
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