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.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy import select, text
from src.api.auth import router as auth_router
from src.api.dashboard import router as dashboard_router
from src.api.git_repositories import router as git_repositories_router
@@ -32,7 +30,7 @@ from src.logging_config import (
RequestLoggingMiddleware,
configure_logging,
)
from src.models.tool_type import ToolType
from src.seeds.builtin_tool_types import seed_builtin_tool_types
# Configure logging early
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
@@ -104,155 +102,6 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
)
async def _table_exists(session, table_name: str) -> bool:
"""Check if a table exists in the database."""
try:
result = await session.execute(
text("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = :table_name
)
"""),
{"table_name": table_name},
)
return result.scalar() or False
except Exception:
return False
async def seed_builtin_tool_types():
async with SessionLocal() as session:
# Check if tool_types table exists before attempting to seed
if not await _table_exists(session, "tool_types"):
logger.warning(
"tool_types table does not exist. Skipping seeding. "
"Migrations may not have run yet."
)
return
builtin_types = [
{
"name": "code-server",
"display_name": "VS Code Server",
"description": "VS Code running in the browser via code-server",
"category": "editor",
"interfaces": ["web"],
"compose_template": """version: "3.8"
services:
code-server:
image: lscr.io/linuxserver/code-server:latest
container_name: {{TOOL_NAME}}
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/London
volumes:
- {{REPO_PATH}}:/config/workspace
ports:
- "8443:8443"
restart: unless-stopped""",
"default_port": 8443,
"required_variables": ["REPO_PATH", "TOOL_NAME"],
},
{
"name": "jupyter-notebook",
"display_name": "Jupyter Notebook",
"description": "Jupyter Lab for interactive development",
"category": "notebook",
"interfaces": ["web"],
"default_port": 8888,
"compose_template": """version: "3.8"
services:
jupyter:
image: jupyter/scipy-notebook:latest
container_name: {{TOOL_NAME}}
environment:
- JUPYTER_ENABLE_LAB=yes
volumes:
- {{REPO_PATH}}:/home/jovyan/work
ports:
- "8888:8888"
restart: unless-stopped""",
"required_variables": ["REPO_PATH", "TOOL_NAME"],
},
{
"name": "opencode",
"display_name": "OpenCode",
"description": "AI coding assistant - run opencode in terminal",
"category": "ai-assistant",
"interfaces": ["terminal"],
"default_port": 3000,
"compose_template": """version: "3.8"
services:
opencode:
image: node:20-slim
container_name: {{TOOL_NAME}}
working_dir: /workspace
environment:
- HOME=/tmp
volumes:
- {{REPO_PATH}}:/workspace
- opencode_home:/tmp
ports:
- "3000:3000"
command: >
sh -c "set -x &&
apt-get update && apt-get install -y git ca-certificates &&
echo 'Installing opencode...' &&
npm install -g opencode-ai 2>&1 || echo 'ERROR: npm install failed' &&
which opencode || echo 'ERROR: opencode not in PATH' &&
npm bin -g &&
ls -la $(npm bin -g) || echo 'ERROR: global bin dir not found' &&
echo 'export PATH=\"$(npm bin -g):\$PATH\"' >> /root/.bashrc &&
echo 'cd /workspace' >> /root/.bashrc &&
echo 'OpenCode installation complete' &&
cd /workspace &&
exec tail -f /dev/null"
stdin_open: true
tty: true
restart: unless-stopped
volumes:
opencode_home:""",
"required_variables": ["REPO_PATH", "TOOL_NAME"],
},
]
for tool_data in builtin_types:
existing = await session.scalar(select(ToolType).where(ToolType.name == tool_data["name"]))
if not existing:
tool_type = ToolType(
name=tool_data["name"],
display_name=tool_data["display_name"],
description=tool_data["description"],
category=tool_data["category"],
interfaces=tool_data["interfaces"],
definition_type="compose",
compose_template=tool_data["compose_template"],
required_variables=tool_data["required_variables"],
default_port=tool_data.get("default_port"),
is_builtin=True,
)
session.add(tool_type)
logger.info("Created built-in tool type: %s", tool_data["name"])
else:
# Update existing built-in tool types to reflect code changes
existing.display_name = tool_data["display_name"]
existing.description = tool_data["description"]
existing.category = tool_data["category"]
existing.interfaces = tool_data["interfaces"]
existing.definition_type = "compose"
existing.compose_template = tool_data["compose_template"]
existing.required_variables = tool_data["required_variables"]
existing.default_port = tool_data.get("default_port")
logger.info("Updated built-in tool type: %s", tool_data["name"])
await session.commit()
logger.info("Built-in tool types seeded successfully.")
@app.on_event("startup")
async def on_startup():
logger.info("Starting up Headquarter API...")
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 type {
ConfigFolder,
CreateConfigFolderRequest,
UpdateConfigFolderRequest,
ProjectOverrideRequest,
} from "../types/config-folder";
export interface ConfigFolder {
id: string;
user_id: string;
name: string;
description: string | null;
mount_path: string;
files: Record<string, string>;
project_overrides: Record<string, { mount_path?: string; files?: Record<string, string> }> | null;
is_active: boolean;
created_at: string;
updated_at: string;
}
export interface CreateConfigFolderRequest {
name: string;
description?: string;
mount_path: string;
files?: Record<string, string>;
is_active?: boolean;
}
export interface UpdateConfigFolderRequest {
name?: string;
description?: string;
mount_path?: string;
files?: Record<string, string>;
is_active?: boolean;
}
export interface ProjectOverrideRequest {
mount_path?: string;
files?: Record<string, string>;
}
export type {
ConfigFolder,
CreateConfigFolderRequest,
UpdateConfigFolderRequest,
ProjectOverrideRequest,
} from "../types/config-folder";
export const listConfigFolders = async (): Promise<ConfigFolder[]> => {
const response = await apiClient.get<ConfigFolder[]>("/config-folders");
@@ -55,7 +34,10 @@ export const updateConfigFolder = async (
id: string,
data: UpdateConfigFolderRequest
): Promise<ConfigFolder> => {
const response = await apiClient.put<ConfigFolder>(`/config-folders/${id}`, data);
const response = await apiClient.put<ConfigFolder>(
`/config-folders/${id}`,
data
);
return response.data;
};
+21 -87
View File
@@ -1,32 +1,26 @@
import { apiClient } from "./client";
import type {
CommitDetail,
CommitHistoryResponse,
CommitResponse,
GitRepository,
GitRepositoryCreate,
GitStatus,
MergeResponse,
URLParseResult,
} from "../types/git-repository";
export interface GitRepository {
id: string;
name: string;
path: string;
project_id: string;
owner_id: string;
is_mirror: boolean;
remote_url: string | null;
last_push: string | null;
created_at: string | null;
}
export interface GitRepositoryCreate {
name: string;
remote_url?: string;
force_original_url?: boolean;
}
export interface URLParseResult {
original_url: string;
base_url: string | null;
is_valid_clone_url: boolean;
needs_parsing: boolean;
host: string | null;
message: string;
error_code: string | null;
}
export type {
CommitDetail,
CommitHistoryEntry,
CommitHistoryResponse,
CommitResponse,
GitRepository,
GitRepositoryCreate,
GitStatus,
MergeResponse,
URLParseResult,
} from "../types/git-repository";
export async function parseGitUrl(url: string): Promise<URLParseResult> {
const response = await apiClient.post("/projects/repositories/parse-url", { url });
@@ -50,24 +44,6 @@ export async function deleteRepository(projectId: string, repoId: string): Promi
await apiClient.delete(`/projects/${projectId}/repositories/${repoId}`);
}
export interface CommitHistoryEntry {
hash: string;
short_hash: string;
message: string;
author_name: string;
author_email: string;
author_date: string;
refs: string[];
graph_symbol: string;
graph_depth: number;
}
export interface CommitHistoryResponse {
commits: CommitHistoryEntry[];
branches: string[];
tags: string[];
}
export async function getRepositoryHistory(
projectId: string,
repoId: string,
@@ -83,25 +59,6 @@ export async function getRepositoryHistory(
return response.data;
}
export interface CommitDetail {
hash: string;
short_hash: string;
message: string;
author_name: string;
author_email: string;
author_date: string;
committer_name: string;
committer_email: string;
committer_date: string;
stats: {
additions: number;
deletions: number;
files_changed: number;
};
diff: string;
parents: string[];
}
export async function getCommitDetail(
projectId: string,
repoId: string,
@@ -113,19 +70,6 @@ export async function getCommitDetail(
return response.data;
}
// Git Control API
export interface GitStatus {
branch: string;
modified: string[];
added: string[];
deleted: string[];
untracked: string[];
renamed: string[];
ahead: number;
behind: number;
}
export async function getRepositoryStatus(
projectId: string,
repoId: string
@@ -173,11 +117,6 @@ export async function checkoutBranch(
return response.data;
}
export interface CommitResponse {
commit_hash: string;
message: string;
}
export async function commitChanges(
projectId: string,
repoId: string,
@@ -225,11 +164,6 @@ export async function pushRepository(
return response.data;
}
export interface MergeResponse {
commit_hash: string;
message: string;
}
export async function mergeBranches(
projectId: string,
repoId: string,
+4 -26
View File
@@ -1,31 +1,9 @@
import { apiClient } from "./client";
import type { Session } from "../types/session";
import type { ToolInstance } from "../types/tool-instance";
export interface ToolInstance {
id: string;
name: string;
display_name: string;
tool_type_id: string;
tool_type_name: string;
tool_type_interfaces: string[];
status: string;
url: string | null;
port: number | null;
created_at: string;
}
export interface Session {
id: string;
display_name: string;
tool_type_name: string;
tool_icon: string;
tool_type_interfaces: string[];
repository_name: string;
repository_id: string;
project_name: string;
project_id: string;
status: string;
url: string | null;
}
export type { Session } from "../types/session";
export type { ToolInstance } from "../types/tool-instance";
export async function listInstances(
projectId: string,
+4 -30
View File
@@ -1,33 +1,7 @@
import { apiClient } from "./client";
import type { ToolConfig, CreateToolConfigRequest } from "../types/tool-config";
export interface ToolConfig {
id: string;
tool_type_id: string;
project_id: string | null;
key: string;
value: string;
config_type: string;
file_path: string | null;
port_override: number | null;
start_command: string | null;
working_directory: string | null;
environment_variables: Record<string, string> | null;
volumes: Array<{ source: string; target: string; type?: string }> | null;
}
export interface CreateToolConfigRequest {
tool_type_id: string;
project_id?: string;
key: string;
value: string;
config_type?: string;
file_path?: string;
port_override?: number;
start_command?: string;
working_directory?: string;
environment_variables?: Record<string, string>;
volumes?: Array<{ source: string; target: string; type?: string }>;
}
export type { ToolConfig, CreateToolConfigRequest } from "../types/tool-config";
export const listToolConfigs = async (
tool_type_id?: string,
@@ -36,7 +10,7 @@ export const listToolConfigs = async (
const params = new URLSearchParams();
if (tool_type_id) params.append("tool_type_id", tool_type_id);
if (project_id) params.append("project_id", project_id);
const response = await apiClient.get<{ configs: ToolConfig[] }>(
`/tool-configs?${params.toString()}`
);
@@ -72,4 +46,4 @@ export const getToolConfigDefaults = async (
`/tool-configs/defaults/${toolTypeId}`
);
return response.data;
};
};
+2 -54
View File
@@ -1,59 +1,7 @@
import { apiClient } from "./client";
import type { ToolType, CreateToolTypeRequest, UpdateToolTypeRequest } from "../types/tool-type";
export interface ReadinessProbe {
command: string;
timeout: number;
interval: number;
}
export interface ToolType {
id: string;
name: string;
display_name: string;
description: string | null;
category: string;
interfaces: string[];
default_port: number | null;
definition_type: 'compose' | 'dockerfile';
compose_template: string | null;
dockerfile_template: string | null;
build_context: Record<string, string> | null;
readiness_probe: ReadinessProbe | null;
required_variables: string[];
is_builtin: boolean;
created_by_id: string | null;
created_at: string;
updated_at: string;
}
export interface CreateToolTypeRequest {
name: string;
display_name: string;
description?: string;
category?: string;
interfaces?: string[];
default_port: number;
definition_type?: 'compose' | 'dockerfile';
compose_template?: string;
dockerfile_template?: string;
build_context?: Record<string, string>;
readiness_probe?: ReadinessProbe;
required_variables: string[];
}
export interface UpdateToolTypeRequest {
display_name?: string;
description?: string;
category?: string;
interfaces?: string[];
default_port?: number;
definition_type?: 'compose' | 'dockerfile';
compose_template?: string;
dockerfile_template?: string;
build_context?: Record<string, string>;
readiness_probe?: ReadinessProbe;
required_variables?: string[];
}
export type { ReadinessProbe, ToolType, CreateToolTypeRequest, UpdateToolTypeRequest } from "../types/tool-type";
export const listToolTypes = async (): Promise<ToolType[]> => {
const response = await apiClient.get<ToolType[]>("/tool-types");
+1 -1
View File
@@ -2,7 +2,7 @@ import { useCallback, useEffect } from "react";
import { Link, NavLink, Outlet } from "react-router-dom";
import { getUserSessions } from "../api/sessions";
import type { Session } from "../api/sessions";
import type { Session } from "../types/session";
import { useTheme } from "../hooks/use-theme";
import { useAuth } from "../state/auth";
import { useSessions } from "../state/sessions";
+2 -2
View File
@@ -1,7 +1,8 @@
import { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Icon } from "./icon";
import type { ToolInstance } from "../api/sessions";
import type { ToolInstance } from "../types/tool-instance";
import type { ToolType } from "../types/tool-type";
import {
checkInstanceHealth,
createInstance,
@@ -12,7 +13,6 @@ import {
startInstance,
stopInstance,
} from "../api/sessions";
import type { ToolType } from "../api/tool_types";
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
@@ -1,7 +1,8 @@
import React, { useCallback, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { deleteRepository, listRepositories, type GitRepository } from "../api/git_repositories";
import type { GitRepository } from "../types/git-repository";
import { deleteRepository, listRepositories } from "../api/git_repositories";
import { RepositoryCreateDialog } from "./repository-create-dialog";
import { Icon } from "./icon";
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from "react";
import { createRepository, parseGitUrl, type GitRepositoryCreate, type URLParseResult } from "../api/git_repositories";
import type { GitRepositoryCreate, URLParseResult } from "../types/git-repository";
import { createRepository, parseGitUrl } from "../api/git_repositories";
import { Icon } from "./icon";
type CreateMode = "clone" | "blank";
+7 -4
View File
@@ -2,12 +2,15 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { getDashboardSummary, type DashboardSummary } from "../api/dashboard";
import { createInstance, getUserSessions, startInstance, stopInstance, deleteInstance, recreateInstanceTunnel, type Session as SessionApi } from "../api/sessions";
import { createInstance, getUserSessions, startInstance, stopInstance, deleteInstance, recreateInstanceTunnel } from "../api/sessions";
import { listProjects } from "../api/projects";
import { listRepositories, type GitRepository } from "../api/git_repositories";
import { listToolTypes, type ToolType } from "../api/tool_types";
import { listRepositories } from "../api/git_repositories";
import { listToolTypes } from "../api/tool_types";
import { updateUserConfig } from "../api/settings";
import type { Project } from "../types";
import type { Project } from "../types/project";
import type { Session as SessionApi } from "../types/session";
import type { GitRepository } from "../types/git-repository";
import type { ToolType } from "../types/tool-type";
import { Icon } from "../components/icon";
type HomeStatus = "loading" | "ready" | "error";
+1 -1
View File
@@ -1,11 +1,11 @@
import { useCallback, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import type { GitRepository } from "../types/git-repository";
import {
deleteRepository,
listRepositories,
} from "../api/git_repositories";
import type { GitRepository } from "../api/git_repositories";
import { Icon } from "../components/icon";
import { RepositoryCreateDialog } from "../components/repository-create-dialog";
+2 -2
View File
@@ -4,10 +4,11 @@ import { Icon } from "../components/icon";
import { Link, useParams, useSearchParams } from "react-router-dom";
import { apiClient } from "../api/client";
import type { GitRepository } from "../types/git-repository";
import type { ToolType } from "../types/tool-type";
import {
getRepositoryStatus,
listRepositories,
type GitRepository,
type GitStatus,
} from "../api/git_repositories";
import { CommitPanel } from "../components/commit-panel";
@@ -16,7 +17,6 @@ import { GitToolbar } from "../components/git-toolbar";
import { InstanceList } from "../components/instance-list";
import { WorkspaceHeader } from "../components/workspace-header";
import { listToolTypes } from "../api/tool_types";
import type { ToolType } from "../api/tool_types";
type WorkspaceStatus = "loading" | "ready" | "error" | "empty";
+7 -5
View File
@@ -2,20 +2,22 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { listProjects } from "../api/projects";
import type { Project } from "../types";
import { listRepositories, type GitRepository } from "../api/git_repositories";
import { listRepositories } from "../api/git_repositories";
import {
getUserSessions,
type Session,
deleteInstance,
stopInstance,
startInstance,
checkInstanceHealth,
recreateInstanceTunnel,
createInstance,
} from "../api/sessions";
import { listToolTypes, type ToolType } from "../api/tool_types";
import { createInstance } from "../api/sessions";
import { listToolTypes } from "../api/tool_types";
import { getUserConfig, updateUserConfig } from "../api/settings";
import type { Project } from "../types/project";
import type { Session } from "../types/session";
import type { GitRepository } from "../types/git-repository";
import type { ToolType } from "../types/tool-type";
import { Icon } from "../components/icon";
type SessionsStatus = "loading" | "ready" | "error";
+3 -2
View File
@@ -2,13 +2,14 @@ import { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Icon } from "../components/icon";
import { listToolTypes, type ToolType } from "../api/tool_types";
import type { ToolType } from "../types/tool-type";
import type { ToolConfig } from "../types/tool-config";
import { listToolTypes } from "../api/tool_types";
import {
createToolConfig,
deleteToolConfig,
listToolConfigs,
updateToolConfig,
type ToolConfig,
} from "../api/tool_configs";
type ConfigStatus = "loading" | "ready" | "error";
+1 -3
View File
@@ -1,16 +1,14 @@
import { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import type { ToolType, CreateToolTypeRequest, UpdateToolTypeRequest } from "../types/tool-type";
import {
createToolType,
deleteToolType,
listToolTypes,
updateToolType,
type CreateToolTypeRequest,
type UpdateToolTypeRequest,
} from "../api/tool_types";
import { Icon } from "../components/icon";
import type { ToolType } from "../api/tool_types";
type ToolTypesStatus = "loading" | "ready" | "error";
type DialogMode = "none" | "create" | "edit";
+2 -13
View File
@@ -1,18 +1,7 @@
import { createContext, useCallback, useContext, useState, type ReactNode } from "react";
import type { Session } from "../types/session";
export interface Session {
id: string;
display_name: string;
tool_type_name: string;
tool_icon: string;
tool_type_interfaces: string[];
repository_name: string;
repository_id: string;
project_name: string;
project_id: string;
status: string;
url: string | null;
}
export type { Session } from "../types/session";
interface SessionsContextType {
sessions: Session[];
+4 -17
View File
@@ -1,18 +1,5 @@
export type SessionUser = {
id: string;
email: string;
name: string;
avatar_url: string | null;
};
// Legacy types file — being migrated to types/ directory.
// This file will be removed once all consumers are updated.
export type SessionPayload = {
user: SessionUser;
};
export type Project = {
id: string;
name: string;
description: string | null;
owner_id: string;
default_ssh_key_id: string | null;
};
export type { Project } from "./types/project";
export type { SessionPayload, SessionUser } from "./types/user";
+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