docs: add OpenSpec change for backend-frontend refactoring
Recovers and adapts the structural refactoring from overwritten
main merge (b6f89f9) to current dev reality.
Scope:
- Schema extraction into apps/api/src/schemas/
- Docker service split into services/docker/ package
- Instance lifecycle extraction from api/tool_instances.py
- Config profile service extraction from api/config_profiles.py
- Auth dependency refactor (get_current_user)
- Frontend reorganization into features/ dirs + kebab-case naming
Exclusions (already in dev): seeding, defaults, unique constraint,
SSH key mounting, terminal backend, tunnel regex, session auto-numbering.
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
## Context
|
||||
|
||||
Current `dev` has all behavioral features from the overwritten `main` merge, but the code structure is pre-refactor:
|
||||
- Monolithic `api/tool_instances.py` (~3000 lines)
|
||||
- Monolithic `api/config_profiles.py` (~1000 lines)
|
||||
- Monolithic `services/docker.py`
|
||||
- No `schemas/` directory
|
||||
- Flat frontend component structure with inconsistent naming
|
||||
|
||||
The `b6f89f9` merge from `main` had a clean refactoring that we need to redo, but adapted to our current reality.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Extract Pydantic schemas from API routers into `src/schemas/`
|
||||
- Split `services/docker.py` into `services/docker/` package
|
||||
- Extract instance lifecycle logic from `api/tool_instances.py` into `services/instance_lifecycle.py`
|
||||
- Extract config profile business logic from `api/config_profiles.py` into `services/config_profiles.py`
|
||||
- Add `get_current_user` auth dependency and migrate routers that need the full user object
|
||||
- Reorganize frontend components into `features/` directories
|
||||
- Standardize frontend API file naming to kebab-case
|
||||
- Standardize frontend page naming to `*Page.tsx`
|
||||
|
||||
**Non-Goals:**
|
||||
- Changing any API request/response shapes
|
||||
- Changing any database schemas
|
||||
- Adding new features
|
||||
- Modifying frontend component behavior or styling
|
||||
- Converting `ConfigProfile` mounts from JSON to relation tables (out of scope — would require migration)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Schema Extraction: One File Per Domain
|
||||
**Decision:** Each domain gets its own schema file: `schemas/tool_type.py`, `schemas/tool_instance.py`, etc.
|
||||
**Rationale:** Keeps schemas close to their domain. Avoids a giant `schemas.py`.
|
||||
|
||||
### 2. Docker Service Split: Functional Boundaries
|
||||
**Decision:** Split by responsibility:
|
||||
- `compose.py` — compose file generation, modification, port injection, network injection
|
||||
- `container.py` — container status, IP lookup, network connect, logs
|
||||
- `config_staging.py` — staging config files into instance directories
|
||||
- `tunnel.py` — extracting tunnel URLs from cloudflared output
|
||||
**Rationale:** Each module has a single reason to change. `docker.py` mixed compose logic with container runtime queries.
|
||||
|
||||
### 3. Instance Lifecycle: Service Receives Raw Params, Not Request Objects
|
||||
**Decision:** Service functions receive model instances and primitive parameters, not FastAPI request objects.
|
||||
**Example:** `create_instance(session, user, project, repo, tool_type, data: CreateInstanceRequest)` → service extracts fields.
|
||||
**Rationale:** Keeps service layer independent of HTTP framework. Easier to test.
|
||||
|
||||
### 4. Auth Pattern: Gradual Migration, Not Big Bang
|
||||
**Decision:** Add `get_current_user` alongside existing `get_current_user_id`. Migrate routers incrementally.
|
||||
**Rationale:** Reduces risk. Endpoints that only need the ID can keep the old pattern.
|
||||
|
||||
### 5. Frontend Naming: Align with `b6f89f9` Conventions
|
||||
**Decision:** Use kebab-case for API files, PascalCase for page files with `Page` suffix, `features/` for component directories.
|
||||
**Rationale:** Matches the `b6f89f9` structure that was already reviewed and accepted.
|
||||
|
||||
## Module Map
|
||||
|
||||
### Backend — Before
|
||||
```
|
||||
api/
|
||||
tool_instances.py (~3000 lines) — HTTP + Docker + Git + Lifecycle
|
||||
config_profiles.py (~1000 lines) — HTTP + Validation + Defaults
|
||||
tool_types.py (~500 lines) — HTTP + Schemas
|
||||
health.py (~150 lines) — HTTP + Schemas
|
||||
users.py (~100 lines) — HTTP + Schemas
|
||||
...
|
||||
services/
|
||||
docker.py (~600 lines) — Compose + Container + Tunnel
|
||||
```
|
||||
|
||||
### Backend — After
|
||||
```
|
||||
schemas/
|
||||
tool_type.py (~200 lines)
|
||||
tool_instance.py (~40 lines)
|
||||
config_profile.py (~130 lines)
|
||||
health.py (~50 lines)
|
||||
user.py (~20 lines)
|
||||
...
|
||||
api/
|
||||
tool_instances.py (~300 lines) — HTTP routing only
|
||||
config_profiles.py (~200 lines) — HTTP routing only
|
||||
tool_types.py (~250 lines) — HTTP + validation endpoints
|
||||
health.py (~80 lines) — HTTP only
|
||||
users.py (~60 lines) — HTTP only
|
||||
...
|
||||
services/
|
||||
instance_lifecycle.py (~420 lines) — Create/Start/Stop/Restart/Delete
|
||||
config_profiles.py (~300 lines) — CRUD + Defaults + Validation
|
||||
docker/
|
||||
__init__.py (~40 lines) — Re-exports
|
||||
compose.py (~240 lines) — Compose generation
|
||||
container.py (~120 lines) — Container queries
|
||||
config_staging.py (~80 lines) — File staging
|
||||
tunnel.py (~150 lines) — Tunnel URL extraction
|
||||
```
|
||||
|
||||
### Frontend — Before
|
||||
```
|
||||
src/
|
||||
api/
|
||||
tool_types.ts
|
||||
ssh_keys.ts
|
||||
git_repositories.ts
|
||||
sessions.ts
|
||||
components/
|
||||
git-toolbar.tsx
|
||||
file-editor.tsx
|
||||
commit-dialog.tsx
|
||||
...
|
||||
pages/
|
||||
dashboard.tsx
|
||||
projects.tsx
|
||||
sessions.tsx
|
||||
...
|
||||
```
|
||||
|
||||
### Frontend — After
|
||||
```
|
||||
src/
|
||||
api/
|
||||
tool-types.ts
|
||||
ssh-keys.ts
|
||||
git-repositories.ts
|
||||
sessions.ts
|
||||
components/
|
||||
features/
|
||||
git/
|
||||
GitToolbar.tsx
|
||||
FileBrowser.tsx
|
||||
FileEditor.tsx
|
||||
CommitDialog.tsx
|
||||
MergeDialog.tsx
|
||||
WorkspaceSidebar.tsx
|
||||
dashboard/
|
||||
DashboardSummary.tsx
|
||||
ActiveSessionsList.tsx
|
||||
ProjectsSection.tsx
|
||||
QuickCreateForm.tsx
|
||||
RecentSessionsSection.tsx
|
||||
project/
|
||||
RepositoriesSettingsTab.tsx
|
||||
tool-workshop/
|
||||
ToolTypesTab.tsx
|
||||
ProtectedRoute.tsx
|
||||
AppShell.tsx
|
||||
pages/
|
||||
DashboardPage.tsx
|
||||
ProjectsPage.tsx
|
||||
SessionsPage.tsx
|
||||
...
|
||||
```
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**[Risk] Import cycles during extraction** → **Mitigation:** Extract schemas first (no service dependencies), then services, then thin routers last. Use TYPE_CHECKING guards.
|
||||
|
||||
**[Risk] Merge conflicts with in-flight features** → **Mitigation:** Coordinate timing. This refactor should be the only large change on `dev` while it's in progress. Freeze other backend work.
|
||||
|
||||
**[Risk] Frontend renaming breaks imports** → **Mitigation:** Use `git mv` for renames so git tracks history. Update all imports in a single commit.
|
||||
|
||||
**[Risk] Missing re-export in docker/__init__.py breaks consumers** → **Mitigation:** After splitting, run a full import test across all backend files. Add any missing re-exports.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. **Phase 1: Schemas** — Extract all Pydantic models into `schemas/`. Update imports in API routers. No logic changes.
|
||||
2. **Phase 2: Services** — Split `docker.py`, extract `instance_lifecycle.py`, extract `config_profiles.py`. Update imports.
|
||||
3. **Phase 3: Auth** — Add `get_current_user`, migrate routers that need the full user object.
|
||||
4. **Phase 4: Frontend** — Rename files, move components, update imports.
|
||||
5. **Phase 5: Verification** — Run full test suite, typecheck, build.
|
||||
Reference in New Issue
Block a user