diff --git a/.env b/.env deleted file mode 100755 index 08baf92..0000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -PYTHONPATH=backend \ No newline at end of file diff --git a/.gitignore b/.gitignore index ac64084..d3c8971 100755 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ .idea data *.sql +.env +screenshots/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8a5ba0f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,321 @@ +# AGENTS.md - Aurora Admin Panel + +## Project Overview + +Aurora is a multi-server port forwarding and relay management panel. Administrators use it to: + +- Add and manage remote servers over SSH +- Allocate ports and deployments to users +- Configure forwarding rules across multiple relay methods +- Monitor server and traffic metrics in real time +- Manage uploaded files and generated configs +- Define reusable service definitions and deploy them to servers + +The control plane runs as a local Docker Compose stack and manages remote relay servers through backend tasks, SSH, and optional Ansible helpers. + +## Repository Structure + +This repository is a monorepo with four Git submodules: + +```text +aurora/ +├── backend/ # FastAPI + Strawberry GraphQL backend (submodule) +├── frontend/ # Active React/Vite admin frontend (submodule) +├── frontend-old/ # Deprecated legacy frontend (submodule, do not modify) +├── deploy/ # Deployment configs and install scripts (submodule) +├── docker-compose.yml # Development orchestration entry point +├── AGENTS.md # Canonical repo guidance +└── CLAUDE.md # Symlink to AGENTS.md +``` + +## Architecture + +```text +Client :8060 → nginx-proxy → /api/* → backend:8888 + → /* → frontend:5173 + +db.localhost:8060 → nginx-proxy → adminer:8080 + +backend → PostgreSQL + TimescaleDB + → Redis + +worker → Redis job queue + → PostgreSQL + → SSH / orchestration on managed servers +``` + +GraphQL subscriptions are served from `/api/graphql` over WebSocket and pass through `nginx-proxy`. + +### Key Services + +| Service | Port | Purpose | +|---|---:|---| +| `nginx-proxy` | `8060` | Reverse proxy for `aurora.localhost` and `db.localhost` | +| `backend` | `8888` | FastAPI app, GraphQL API, auth, uploads | +| `worker` | - | Huey worker for background jobs and SSH orchestration | +| `frontend` | `5173` | Vite dev server for the active frontend | +| `postgres` | `5432` | PostgreSQL + TimescaleDB | +| `redis` | `6379` | Cache, queue, pub/sub | +| `adminer` | proxied via `db.localhost:8060` | Database admin UI | + +## Backend (`backend/`) + +### Tech Stack + +- FastAPI +- Strawberry GraphQL +- SQLAlchemy 1.4 with async `asyncpg` +- Alembic +- Huey +- Redis +- Fabric / Paramiko + +### Important Backend Areas + +```text +backend/app/ +├── main.py # FastAPI app setup and route mounting +├── initial_data.py # Seed initial superuser +├── core/ # Settings, auth, security, Redis clients +├── graphql/ # Primary API surface +├── db/ +│ ├── models/ # SQLAlchemy models +│ ├── crud/ # Data access helpers +│ ├── schemas/ # Pydantic schema definitions +│ ├── session.py # Sync session +│ └── async_session.py # Async session +├── api/ # Legacy REST endpoints +├── alembic/ # Migrations +└── utils/ # Compilation, permissions, file helpers + +backend/tasks/ +├── server.py # Main task entrypoints +├── functions/ # Forwarding-method implementations +└── utils/ # SSH, orchestration, metrics, traffic +``` + +### GraphQL API + +- Endpoint: `/api/graphql` +- Transport: HTTP queries/mutations + WebSocket subscriptions +- Auth: JWT bearer token, with `POST /api/token` still used for login +- Permission classes live in `backend/app/graphql/permission.py` + +Current GraphQL modules include: + +- `auth.py` +- `deployment.py` +- `file.py` +- `metric.py` +- `port.py` +- `port_forward.py` +- `server.py` +- `service_definition.py` +- `task.py` +- `user.py` + +### Core Models + +Core data models include: + +- `User` +- `Server` +- `Port` +- `PortForwardRule` +- `File` +- `ServerDeployment` +- `ServiceDefinition` +- `ServiceBinding` +- time-series metric models in `metric.py` + +### Common Backend Commands + +```bash +docker compose up -d +docker compose logs -f backend +docker compose logs -f worker +docker compose exec backend alembic upgrade heads +docker compose exec backend python app/initial_data.py +docker compose exec backend pytest +docker compose exec backend alembic revision --autogenerate -m "description" +``` + +## Frontend (`frontend/`) + +### Tech Stack + +- React 19 +- TypeScript +- Vite 7 +- Tailwind CSS 4 +- shadcn/ui component layer on top of Radix primitives +- Apollo Client 3 with `graphql-ws` and `apollo-upload-client` +- Jotai +- React Router 6 +- React Hook Form +- i18next +- Framer Motion +- Recharts + +### Frontend Structure + +```text +frontend/src/ +├── main.tsx # Root providers and app mount +├── App.tsx # Route tree +├── Layout.tsx # Authenticated shell +├── routes.ts # Canonical route config +├── graphql.ts # Apollo client setup +├── index.css # Tailwind v4 theme tokens and globals +├── components/ +│ ├── theme-provider.tsx +│ └── ui/ # shadcn/Radix-based primitives +├── atoms/ # Jotai atoms and reducers +├── features/ +│ ├── auth/ +│ ├── server/ +│ ├── port/ +│ ├── user/ +│ ├── file/ +│ ├── deployment/ +│ ├── service-editor/ +│ ├── layout/ +│ ├── modal/ +│ ├── theme/ +│ ├── i18n/ +│ └── ui/ +├── hooks/ +├── queries/ # GraphQL operations +├── types/generated.ts # Generated GraphQL types +└── utils/ +``` + +### Frontend Runtime Patterns + +- Root provider stack in `main.tsx` is: + `ApolloProvider` → `ThemeProvider` → `TooltipProvider` → `HelmetProvider` → `Suspense` +- shadcn/Radix primitives live under `src/components/ui/` +- Global app state is handled with Jotai atoms in `src/atoms/` +- Route metadata lives in `src/routes.ts` +- GraphQL operations live in `src/queries/` +- The active app is TypeScript-first; new work should follow the TS/shadcn structure, not the old JSX/DaisyUI layout + +### Current Routes + +```text +/login +/create-account +/app + /app/servers + /app/servers/:serverId + /app/servers/:serverId/ports + /app/servers/:serverId/users + /app/users + /app/files + /app/services + /app/services/editor + /app/services/editor/:serviceId + /app/about + /app/themes +``` + +`/app/deployments` currently redirects back to the server area rather than owning a separate page. + +### Auth Flow + +1. `POST /api/token` with email/password +2. JWT is stored in localStorage via Jotai `authAtom` +3. `frontend/src/graphql.ts` injects the bearer token for GraphQL requests +4. GraphQL subscriptions connect to `ws:///api/graphql` +5. Permission state is derived from the decoded JWT payload + +### Common Frontend Commands + +```bash +docker compose logs -f frontend +cd frontend && npm run dev +cd frontend && npm run build +``` + +Access the active app through `http://aurora.localhost:8060`. + +## Service Definition System + +Aurora now uses service-definition terminology in the UI and database model layer. + +### Backend + +- Schema model: `backend/app/db/schemas/service_definition.py` +- DB model: `backend/app/db/models/service_definition.py` +- GraphQL API: `backend/app/graphql/service_definition.py` +- Compiler: `backend/app/utils/service_definition.py` + +`compile_service_preview()` validates authoring JSON, coerces submitted values, applies conditions, emits args/env/files/stdin, and returns a preview plan. + +### Frontend + +The service editor lives in `frontend/src/features/service-editor/` and includes: + +- `ServiceListPage.tsx` +- `ServiceEditorPage.tsx` +- `ParamEditorPanel.tsx` +- `serviceAdapter.ts` +- `useDynamicForm.tsx` +- preview panels for authoring JSON, compiled output, and rendered forms + +The deployment UI also consumes the same service-definition schema to render parameter forms dynamically. + +### Naming Quirk + +The rename from contracts to services is not fully complete at the schema boundary yet: + +- authoring JSON still uses `contractKey` +- some compiler/template variables still reference `contractKey` +- `compileServicePreview` still takes a `contract` JSON argument + +When updating this area, prefer service terminology in new UI and model code, but expect those schema names to remain until a dedicated cleanup pass lands. + +## Development Workflow + +### Starting the Stack + +```bash +docker compose up -d +docker compose exec backend alembic upgrade heads +docker compose exec backend python app/initial_data.py +``` + +Then open: + +- `http://aurora.localhost:8060` +- `http://aurora.localhost:8060/api/graphql` +- `http://db.localhost:8060` + +### Typical Change Areas + +- GraphQL API changes: `backend/app/graphql/` +- DB model changes: `backend/app/db/models/` plus Alembic +- Task / SSH orchestration: `backend/tasks/` +- UI components and pages: `frontend/src/features/` and `frontend/src/components/ui/` +- Route and navigation changes: `frontend/src/routes.ts`, `frontend/src/Layout.tsx`, `frontend/src/features/layout/` + +## Test Credentials + +Test credentials are stored in `.env` (gitignored). Use these env vars for browser testing: + +- `AURORA_TEST_EMAIL` — login email +- `AURORA_TEST_PASSWORD` — login password +- `AURORA_PORT` — nginx-proxy host port (default: `8060`) + +Access the app at `http://aurora.localhost:${AURORA_PORT}`. + +## Change Policy + +This is a single-owner project. Large refactors and internal API changes are acceptable. Backward compatibility is not a hard requirement, except for preserving user data and avoiding destructive database migrations unless explicitly intended. + +## Known Quirks + +- `frontend-old/` is deprecated and should not be modified unless explicitly requested. +- REST is still used for login and some legacy flows; GraphQL is the primary application API. +- The service-definition schema still carries `contractKey` naming internally. +- `frontend/src/utils/websocketManager.ts` exists as leftover utility code, but the active app uses GraphQL subscriptions. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/backend b/backend index f6cb617..c24c79b 160000 --- a/backend +++ b/backend @@ -1 +1 @@ -Subproject commit f6cb617c15b8bcf4e24731e3bf20d8c0e93b0731 +Subproject commit c24c79b07e31c7deb120ba83371e9cda00afa1cf diff --git a/deploy b/deploy index bee4b80..f73fd04 160000 --- a/deploy +++ b/deploy @@ -1 +1 @@ -Subproject commit bee4b80acf8e18fcd82586a738a238cac6049e29 +Subproject commit f73fd041a719cfdb6e0a45d53233953d0ea88459 diff --git a/docker-compose.yml b/docker-compose.yml index f9a6af3..8d6d601 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,29 +1,18 @@ -version: '3.7' services: - nginx: - image: nginx:1.17 + nginx-proxy: + image: nginxproxy/nginx-proxy:1.8-alpine + restart: unless-stopped volumes: - - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf - - ./frontend-old/build:/var/www/html + - /var/run/docker.sock:/tmp/docker.sock:ro ports: - - 8000:80 - depends_on: - - backend - - frontend-old - nginx2: - image: nginx:1.17 - volumes: - - ./nginx/nginx_v2.conf:/etc/nginx/conf.d/default.conf - ports: - - 8001:80 - depends_on: - - backend - - frontend + - "8060:80" worker: build: context: backend dockerfile: Dockerfile + args: + BACKEND_APP_VERSION: local command: bash worker.sh environment: ENVIRONMENT: DEV @@ -34,55 +23,43 @@ services: volumes: - /home/lei/.ssh/id_ed25519:/app/ansible/env/ssh_key - ./backend:/app + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy - postgres: - image: postgres:13-alpine - restart: always - environment: - POSTGRES_USER: aurora - POSTGRES_PASSWORD: AuroraAdminPanel321 - ports: - - 5432:5432 - volumes: - - db-data:/var/lib/postgresql/data - backend: build: context: backend dockerfile: Dockerfile args: BACKEND_APP_VERSION: local - command: bash -c "while ! port.id, nullable) — null for services that don't require a port +- Partial unique constraint: `(port_id) WHERE is_active = True AND port_id IS NOT NULL` — one active deployment per port + +**Service definition schema** (`aurora-exec/v1`) — one new top-level field: +- `requiresPort` (boolean, default `true`) — when true, deployment must include a port selection + +No changes to Port, PortUser, ServiceBinding, or DeploymentLog. + +### Compilation & Context Injection + +`compile_service_preview()` changes: +- `context_payload` gains optional `port` key (integer) +- When `requiresPort` is true and `context.port` is present, `{{port}}` is available as a template variable in: param defaults, `baseArgs`, file `pathTemplate`, file content, and any string in the emit pipeline +- If `requiresPort` is true but `context.port` is missing, return `ok: false` with error +- Preview mode (service editor): pass `context.port = 0` as placeholder + +### GraphQL API + +**Mutations** — `deployExecutable` and `deployService` gain optional `portId: Int`: +- If `requiresPort: true`, `portId` is required +- If `requiresPort: false`, `portId` must be null +- Validates: port belongs to target server, user has access, port has no active deployment +- Sets `port_id` on ServerDeployment, passes `Port.num` as `context.port` to compilation + +**Queries:** +- `ServerDeploymentType` gains `port` field +- New query: `availablePortsForDeployment(serverId: Int!) -> [Port!]!` + - Superuser: all ports on any server with no active deployment + - Admin (is_ops on server): all ports on that server with no active deployment + - Normal user: only ports allocated via PortUser with no active deployment + +### Frontend + +**DeployModal:** +- If `requiresPort` is true, show port selector dropdown before param form +- Fetch via `availablePortsForDeployment(serverId)` +- Display as "Port {num}" or "Port {num} -> {external_num}" if external differs +- No available ports = disable deploy with message +- Pass selected port's `num` as `context.port` to auto-compile preview +- Pass `portId` to deploy mutation + +**Deployment list/card UI:** +- Show port number alongside deployment info when present + +**Service editor:** +- No changes — `{{port}}` is a context variable authors type manually diff --git a/docs/plans/2026-03-08-port-bound-deployments.md b/docs/plans/2026-03-08-port-bound-deployments.md new file mode 100644 index 0000000..ef6458c --- /dev/null +++ b/docs/plans/2026-03-08-port-bound-deployments.md @@ -0,0 +1,664 @@ +# Port-Bound Deployments Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Link deployments to allocated ports so services can bind to user-assigned ports via `{{port}}` context variable injection. + +**Architecture:** Add `port_id` FK (nullable) to `ServerDeployment`, `requiresPort` boolean to the service definition schema, and `{{port}}` template variable substitution in `compile_service_preview()`. Frontend shows a port selector when deploying port-requiring services. + +**Tech Stack:** SQLAlchemy 1.4, Alembic, Pydantic, Strawberry GraphQL, React 18, Apollo Client + +--- + +### Task 1: Alembic Migration — add `port_id` to `server_deployment` + +**Files:** +- Create: `backend/app/alembic/versions/_add_port_id_to_server_deployment.py` + +**Step 1: Generate migration** + +Run: `docker-compose exec backend alembic revision --autogenerate -m "add port_id to server_deployment"` + +Then edit the generated migration to contain: + +```python +from alembic import op +import sqlalchemy as sa + +revision = "" +down_revision = "38fd48957fee" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + "server_deployment", + sa.Column("port_id", sa.Integer(), sa.ForeignKey("port.id"), nullable=True), + ) + # Partial unique index: only one active deployment per port + op.create_index( + "ix_server_deployment_port_id_active", + "server_deployment", + ["port_id"], + unique=True, + postgresql_where=sa.text("is_active = true AND port_id IS NOT NULL"), + ) + + +def downgrade(): + op.drop_index("ix_server_deployment_port_id_active", table_name="server_deployment") + op.drop_column("server_deployment", "port_id") +``` + +**Step 2: Run migration** + +Run: `docker-compose exec backend alembic upgrade head` +Expected: Migration applies successfully. + +**Step 3: Commit** + +```bash +git add backend/app/alembic/versions/*add_port_id* +git commit -m "migration: add port_id FK to server_deployment with partial unique index" +``` + +--- + +### Task 2: Update ServerDeployment model — add `port_id` column and relationship + +**Files:** +- Modify: `backend/app/db/models/server_deployment.py` + +**Step 1: Add column and relationship** + +In `ServerDeployment` class (after `server_id` column, line 56), add: + +```python +port_id = Column(Integer, ForeignKey("port.id"), nullable=True) +``` + +After the `server` relationship (line 76), add: + +```python +port = relationship("Port", backref="deployment") +``` + +Using `backref` so we don't need to modify `port.py`. The backref gives `Port.deployment` (singular — one active deployment per port). + +**Step 2: Verify import** + +No new imports needed — `Integer`, `ForeignKey`, `Column`, `relationship` are already imported. + +**Step 3: Commit** + +```bash +git add backend/app/db/models/server_deployment.py +git commit -m "feat: add port_id column and relationship to ServerDeployment" +``` + +--- + +### Task 3: Add `requiresPort` to service definition schema + +**Files:** +- Modify: `backend/app/db/schemas/service_definition.py` + +**Step 1: Add field to ServiceDefinitionAuthoringV1** + +At line 297 (after `params`), add: + +```python +requiresPort: bool = True +``` + +No validators needed — it's a simple boolean with a default. + +**Step 2: Add `port` to ALLOWED_TEMPLATE_VARS** + +At line 10, change: + +```python +ALLOWED_TEMPLATE_VARS = {"jobId", "contractKey", "paramKey"} +``` + +to: + +```python +ALLOWED_TEMPLATE_VARS = {"jobId", "contractKey", "paramKey", "port"} +``` + +**Step 3: Commit** + +```bash +git add backend/app/db/schemas/service_definition.py +git commit -m "feat: add requiresPort field to service definition schema" +``` + +--- + +### Task 4: Inject `{{port}}` in `compile_service_preview()` + +**Files:** +- Modify: `backend/app/utils/service_definition.py` + +**Step 1: Add context variable substitution** + +The `{{port}}` variable needs to work in three places: +1. `baseArgs` — e.g., `["-L", "0.0.0.0:{{port}}"]` +2. Param `default` values — e.g., `"default": "0.0.0.0:{{port}}"` +3. File content and path templates (already uses `_render_path_template`) + +Add a helper function after `_render_path_template` (after line 202): + +```python +def _substitute_context_vars(value: t.Any, context: dict) -> t.Any: + """Recursively substitute {{var}} placeholders in strings using context dict.""" + if isinstance(value, str): + return PATH_TEMPLATE_VAR_RE.sub( + lambda m: str(context.get(m.group(1), m.group(0))), value + ) + if isinstance(value, list): + return [_substitute_context_vars(item, context) for item in value] + if isinstance(value, dict): + return {k: _substitute_context_vars(v, context) for k, v in value.items()} + return value +``` + +**Step 2: Apply substitution in `compile_service_preview()`** + +After the schema validation succeeds (after line 384), add: + +```python +# Validate requiresPort vs context +if contract.requiresPort: + if not context_payload or "port" not in context_payload: + return {"ok": False, "error": "This service requires a port selection"} + +# Substitute context variables in baseArgs and param defaults +ctx = context_payload or {} +if contract.exec.baseArgs: + contract.exec.baseArgs = [ + _substitute_context_vars(a, ctx) for a in contract.exec.baseArgs + ] +for param in contract.params: + if param.default is not None: + param.default = _substitute_context_vars(param.default, ctx) +``` + +**Step 3: Update `_normalize_path_context` to include port** + +At line 189, update the function to pass through the port: + +```python +def _normalize_path_context(contract: ServiceDefinitionAuthoringV1, context: dict, param_key: str): + ctx = { + "jobId": str((context or {}).get("jobId", "preview")), + "contractKey": contract.contractKey, + "paramKey": param_key, + } + if "port" in (context or {}): + ctx["port"] = str(context["port"]) + return ctx +``` + +**Step 4: Commit** + +```bash +git add backend/app/utils/service_definition.py +git commit -m "feat: inject {{port}} context variable in service compilation" +``` + +--- + +### Task 5: Add `availablePortsForDeployment` GraphQL query + +**Files:** +- Modify: `backend/app/graphql/port.py` +- Modify: `backend/app/graphql/schema.py` + +**Step 1: Add resolver to Port class in `port.py`** + +Add this static method to the `Port` class (after `get_port_count`, around line 185): + +```python +@staticmethod +async def get_available_ports_for_deployment( + info: Info, + server_id: int, +) -> List["Port"]: + """Return ports on this server that the current user can access + and that have no active deployment.""" + from app.db.models import ServerDeployment as DBServerDeployment + + user = info.context["request"].state.user + + # Subquery: port_ids with an active deployment + active_deployment_port_ids = ( + select(DBServerDeployment.port_id) + .where( + DBServerDeployment.is_active == True, + DBServerDeployment.port_id.isnot(None), + ) + .scalar_subquery() + ) + + stmt = select(DBPort).where( + DBPort.server_id == server_id, + DBPort.is_active == True, + DBPort.id.notin_(active_deployment_port_ids), + ).order_by(DBPort.num) + + # Access control: superuser sees all, ops sees server ports, others need PortUser + if not user.is_superuser: + if user.is_ops: + # Check user is admin for this server + admin_server_ids = select(DBServerUser.server_id).where( + DBServerUser.user_id == user.id + ) + stmt = stmt.where( + or_( + DBPort.server_id.in_(admin_server_ids), + DBPort.id.in_( + select(DBPortUser.port_id).where(DBPortUser.user_id == user.id) + ), + ) + ) + else: + stmt = stmt.where( + DBPort.id.in_( + select(DBPortUser.port_id).where(DBPortUser.user_id == user.id) + ) + ) + + async with async_db_session() as async_db: + result = await async_db.execute(stmt) + return result.scalars().unique().all() +``` + +**Step 2: Wire query in `schema.py`** + +In the `Query` class (after `paginated_server_deployments`, around line 121), add: + +```python +available_ports_for_deployment: List[Port] = strawberry.field( + resolver=Port.get_available_ports_for_deployment, + permission_classes=[IsAuthenticated], +) +``` + +Add `Port` to imports from `app.graphql.port` if not already there. Also ensure `List` from `typing` is imported. + +**Step 3: Commit** + +```bash +git add backend/app/graphql/port.py backend/app/graphql/schema.py +git commit -m "feat: add availablePortsForDeployment query" +``` + +--- + +### Task 6: Update deploy mutations to accept `portId` + +**Files:** +- Modify: `backend/app/graphql/deployment.py` +- Modify: `backend/app/graphql/schema.py` + +**Step 1: Update `deploy_executable_resolver`** + +Add `port_id: Optional[int] = None` parameter (line 214). Add validation inside the `async with` block, before the server loop: + +```python +# Validate port if provided +port = None +if port_id is not None: + port = (await db.execute( + select(DBPort).where(DBPort.id == port_id) + )).scalars().first() + if not port: + raise ValueError(f"Port {port_id} not found") + # Check port belongs to one of the target servers + if port.server_id not in server_ids: + raise ValueError(f"Port {port_id} does not belong to any of the target servers") + # Check no active deployment on this port + active_on_port = (await db.execute( + select(DBServerDeployment).where( + DBServerDeployment.port_id == port_id, + DBServerDeployment.is_active == True, + ) + )).scalars().first() + if active_on_port: + raise ValueError(f"Port {port_id} already has an active deployment") +``` + +Import `DBPort` at the top of the file: +```python +from app.db.models import Port as DBPort +``` + +Set `port_id` on the deployment object (both create and update paths): +```python +deployment.port_id = port_id # None if no port required +``` + +Note: when `port_id` is set and there are multiple `server_ids`, the port can only belong to one server. The validation above ensures the port's `server_id` is in the list. In practice, port-bound deployments will typically target a single server. + +**Step 2: Update `deploy_service_resolver`** + +Same changes as step 1 — add `port_id: Optional[int] = None` parameter and the same validation block. + +Additionally, after loading the service definition, validate `requiresPort`: + +```python +import json +config = json.loads(service.config_json) if isinstance(service.config_json, str) else service.config_json +requires_port = config.get("requiresPort", True) +if requires_port and port_id is None: + raise ValueError("This service requires a port selection") +if not requires_port and port_id is not None: + raise ValueError("This service does not accept a port") +``` + +Do the same for `deploy_executable_resolver` by loading the service definition through the binding. + +**Step 3: Pass `context.port` to compilation in task dispatch** + +The compilation happens in the Huey task, not in the resolver. The `port_id` is stored on `ServerDeployment`, so the task can load the port. No changes needed in the resolver for compilation — this is handled in Task 7. + +**Step 4: Update schema.py mutation wiring** + +The strawberry field definitions in `schema.py` don't need changes because the resolver signature change is picked up automatically by Strawberry (it inspects the function signature). + +**Step 5: Commit** + +```bash +git add backend/app/graphql/deployment.py +git commit -m "feat: accept portId in deploy mutations with validation" +``` + +--- + +### Task 7: Pass `context.port` in Huey deployment tasks + +**Files:** +- Modify: `backend/tasks/deployment.py` + +**Step 1: Find where `compile_service_preview` is called** + +In the deployment task, find where the service definition is compiled and add `context.port`: + +```python +# After loading the deployment and its port: +context = {"jobId": str(deployment.id)} +if deployment.port_id and deployment.port: + context["port"] = deployment.port.num +``` + +Pass this context to `compile_service_preview()`. + +**Step 2: Ensure port relationship is loaded** + +When loading the deployment in the task, eagerly load the port: + +```python +stmt = select(DBServerDeployment).where( + DBServerDeployment.id == deployment_id +).options(joinedload(DBServerDeployment.port)) +``` + +**Step 3: Commit** + +```bash +git add backend/tasks/deployment.py +git commit -m "feat: pass port number as context variable to service compilation" +``` + +--- + +### Task 8: Update ServerDeploymentType to expose port + +**Files:** +- Modify: `backend/app/graphql/deployment.py` + +**Step 1: Add port field to ServerDeploymentType** + +In the `ServerDeployment` strawberry type, add: + +```python +port_id: Optional[int] +port: Optional[Annotated["Port", strawberry.lazy("app.graphql.port")]] +``` + +Ensure the `set_options` method (if it exists) loads the port relationship when requested. + +**Step 2: Commit** + +```bash +git add backend/app/graphql/deployment.py +git commit -m "feat: expose port on ServerDeploymentType" +``` + +--- + +### Task 9: Frontend — add port query and update deploy mutations + +**Files:** +- Modify: `frontend/src/queries/deployment.js` + +**Step 1: Add available ports query** + +```javascript +export const GET_AVAILABLE_PORTS = gql` + query GetAvailablePorts($serverId: Int!) { + availablePortsForDeployment(serverId: $serverId) { + id + num + externalNum + } + } +`; +``` + +**Step 2: Update deploy mutations to include portId** + +Update `DEPLOY_EXECUTABLE`: +```graphql +mutation DeployExecutable( + $serviceBindingId: Int! + $serverIds: [Int!]! + $values: JSON! + $portId: Int +) { + deployExecutable( + serviceBindingId: $serviceBindingId + serverIds: $serverIds + values: $values + portId: $portId + ) { + id + serviceBindingId + serverId + portId + status + createdAt + updatedAt + } +} +``` + +Update `DEPLOY_SERVICE` similarly with `$portId: Int` variable. + +**Step 3: Add portId to deployment query responses** + +Update `GET_SERVER_DEPLOYMENT` and `GET_PAGINATED_SERVER_DEPLOYMENTS` to include `portId` and `port { num externalNum }` fields. + +**Step 4: Commit** + +```bash +cd frontend && git add src/queries/deployment.js && git commit -m "feat: add port queries and portId to deploy mutations" +``` + +--- + +### Task 10: Frontend — add port selector to DeployModal + +**Files:** +- Modify: `frontend/src/features/deployment/DeployModal.jsx` + +**Step 1: Import and query available ports** + +After the server ID is known (from `modalProps.serverId`), query available ports: + +```javascript +import { useQuery } from "@apollo/client"; +import { GET_AVAILABLE_PORTS } from "../../queries/deployment"; + +// Inside component: +const { data: portsData, loading: portsLoading } = useQuery(GET_AVAILABLE_PORTS, { + variables: { serverId }, + skip: !serverId, +}); +``` + +**Step 2: Add port selector state** + +```javascript +const [selectedPortId, setSelectedPortId] = useState(null); +``` + +**Step 3: Render port selector** + +When `requiresPort` is true on the loaded service definition (from `configJson`), render a select dropdown in Step 2 (before the param form): + +```jsx +{requiresPort && ( +
+ + + {portsData?.availablePortsForDeployment?.length === 0 && ( +

No available ports on this server

+ )} +
+)} +``` + +**Step 4: Pass portId to deploy mutation** + +Update the deploy call (around line 128) to include `portId: selectedPortId`: + +```javascript +// Catalog mode +deployService({ + variables: { + serviceId: selectedService.id, + serverIds: [serverId], + values: formValues, + portId: selectedPortId, + }, +}); + +// Binding mode +deployExecutable({ + variables: { + serviceBindingId: selectedBinding.id, + serverIds: [serverId], + values: formValues, + portId: selectedPortId, + }, +}); +``` + +**Step 5: Pass port to auto-compile context** + +When calling `compileServicePreview` for live preview, include the port number: + +```javascript +const selectedPort = portsData?.availablePortsForDeployment?.find(p => p.id === selectedPortId); +const context = { + jobId: "preview", + ...(selectedPort ? { port: selectedPort.num } : {}), +}; +``` + +**Step 6: Disable deploy button when port required but not selected** + +```javascript +const requiresPort = configJson?.requiresPort !== false; // default true +const canDeploy = !requiresPort || selectedPortId; +``` + +Disable the deploy button when `!canDeploy`. + +**Step 7: Commit** + +```bash +cd frontend && git add src/features/deployment/DeployModal.jsx && git commit -m "feat: add port selector to deploy modal" +``` + +--- + +### Task 11: Show port info on deployment list/cards + +**Files:** +- Modify: relevant deployment list component in `frontend/src/features/deployment/` + +**Step 1: Find the deployment list component** + +Check `frontend/src/features/deployment/` for list or card components that render deployments. + +**Step 2: Display port number** + +Where deployment info is shown, add the port: + +```jsx +{deployment.port && ( + + Port {deployment.port.num} + +)} +``` + +**Step 3: Commit** + +```bash +cd frontend && git add -A && git commit -m "feat: display port number on deployment cards" +``` + +--- + +### Task 12: Update built-in service seeds + +**Files:** +- Modify: `backend/app/seed_services.py` (if it exists) + +**Step 1: Add `requiresPort` to existing built-in service definitions** + +For services that need ports (proxies, forwarders): `requiresPort: true` (or omit, since it defaults to true). + +For services that don't (monitoring agents like node_exporter, iperf): set `requiresPort: false`. + +**Step 2: Update `{{port}}` usage in baseArgs or param defaults** + +For port-requiring services, replace hardcoded port params with `{{port}}` references. For example, a gost service might have: + +```json +"baseArgs": ["-L", "0.0.0.0:{{port}}"] +``` + +**Step 3: Commit** + +```bash +git add backend/app/seed_services.py +git commit -m "feat: set requiresPort and use {{port}} in built-in service definitions" +``` diff --git a/docs/plans/2026-03-08-rename-contracts-to-services-design.md b/docs/plans/2026-03-08-rename-contracts-to-services-design.md new file mode 100644 index 0000000..d6f111c --- /dev/null +++ b/docs/plans/2026-03-08-rename-contracts-to-services-design.md @@ -0,0 +1,150 @@ +# Rename Executable Contracts to Services + +**Date**: 2026-03-08 +**Status**: Approved +**Scope**: Full-stack rename (DB, backend, GraphQL, frontend, i18n) + +## Motivation + +"Executable Contract" as a concept is confusing. The feature's core job is **service lifecycle management** — install, configure, start, stop, update, remove software on relay servers. The mental model should be a **Service Manager**, and the primary noun should be **Service**. + +The underlying schema, compilation engine, and deployment machinery are sound. This is a rename and reframe, not a redesign. + +## Terminology Mapping + +| Current | New | +|---------|-----| +| Executable Contract | Service (Definition) | +| Contract Key | Service Key | +| Contract Builder | Service Editor | +| Contract List | Services | +| File Contract Binding | Service Binding | +| App Catalog | Service Catalog | +| Built-in Contract | Built-in Service | +| Deploy Contract | Deploy Service | + +## Database Changes + +### Table Renames + +| Current | New | +|---------|-----| +| `executable_contract` | `service_definition` | +| `file_contract_binding` | `service_binding` | +| `server_deployment` | unchanged | +| `deployment_log` | unchanged | + +### Column Renames + +| Table | Current | New | +|-------|---------|-----| +| `service_definition` | `contract_key` | `service_key` | +| `service_definition` | `schema_json` | `config_json` | +| `service_binding` | `contract_id` | `service_id` | +| `server_deployment` | `contract_id` | `service_id` | +| `server_deployment` | `binding_id` | `service_binding_id` | + +### Preserved + +- `server_deployment` and `deployment_log` table names (correct for service instances) +- Internal JSON schema version `aurora-exec/v1` (changing it would break stored data) +- JSON field names inside stored payloads (`contractKey`, `exec`, `params`) +- Unique constraint becomes `unique(service_key, version)` + +### Migration + +Single Alembic migration using `ALTER TABLE ... RENAME TO` and `ALTER TABLE ... RENAME COLUMN`. Non-destructive, preserves all data. + +## Backend Changes + +### Models (`app/db/models/`) + +| Current File | New File | Class | +|-------------|----------|-------| +| `executable_contract.py` | `service_definition.py` | `ServiceDefinition` | +| `file_contract_binding.py` | `service_binding.py` | `ServiceBinding` | +| `server_deployment.py` | unchanged | Update FK refs: `contract_id` -> `service_id`, `binding_id` -> `service_binding_id` | + +### Schemas (`app/db/schemas/`) + +- `executable_contract.py` -> `service_definition.py` +- `ExecutableContractAuthoringV1` -> `ServiceDefinitionAuthoringV1` +- Internal JSON keys unchanged + +### Utils (`app/utils/`) + +- `executable_contract.py` -> `service_definition.py` +- `compile_executable_contract_preview()` -> `compile_service_preview()` + +### GraphQL (`app/graphql/`) + +- `executable_contract.py` -> `service_definition.py` +- Type: `ExecutableContractType` -> `ServiceDefinitionType` +- Queries: `executable_contract()` -> `service_definition()`, `paginated_executable_contracts()` -> `paginated_service_definitions()` +- Mutations: `create_executable_contract()` -> `create_service_definition()`, `update_executable_contract()` -> `update_service_definition()`, `delete_executable_contract()` -> `delete_service_definition()` +- Compile: `compile_executable_contract_preview()` -> `compile_service_preview()`, `compile_executable_contract_preview_by_id()` -> `compile_service_preview_by_id()` +- Deployment: `deployContract()` -> `deployService()` + +### Seed + +- `seed_contracts.py` -> `seed_services.py` + +## Frontend Changes + +### Components (`src/features/`) + +| Current | New | +|---------|-----| +| `contract-builder/` | `service-editor/` | +| `ContractBuilderPage.jsx` | `ServiceEditorPage.jsx` | +| `ContractListPage.jsx` | `ServiceListPage.jsx` | +| `ParamBuilderPanel.jsx` | `ParamEditorPanel.jsx` | +| `authoringAdapter.js` | `serviceAdapter.js` | +| `constants.js` | Updated query names | + +Files that keep their names (content updated): `AuthoringJsonPanel.jsx`, `FormPreviewPanel.jsx`, `CompileOutputPanel.jsx`, `useDynamicForm.jsx`, `formUtils.js`, `builderUtils.js`, `fields/*`. + +### Routes + +- Path: `contracts` -> `services` +- Label: `"Contract Builder"` -> `"Services"` +- Full path: `/app/contracts` -> `/app/services` + +### GraphQL Queries + +All query/mutation names updated to match backend. Key changes: +- `GET_CONTRACTS_FOR_BINDING` -> `GET_SERVICES_FOR_BINDING` +- `DEPLOY_CONTRACT` -> `DEPLOY_SERVICE` +- `CREATE_EXECUTABLE_CONTRACT` -> `CREATE_SERVICE_DEFINITION` +- `COMPILE_EXECUTABLE_CONTRACT_PREVIEW` -> `COMPILE_SERVICE_PREVIEW` + +### DeployModal + +- "App Catalog" tab -> "Service Catalog" +- Variable names: `contract` -> `service` +- Labels: "Deploy from contract" -> "Deploy Service" + +### i18n + +Both `en/` and `zh/` translation files updated. All "Contract"/"contract" strings replaced with "Service"/"service" equivalents. + +## Out of Scope + +- No changes to the authoring JSON schema format or compilation logic +- No changes to systemd unit naming (`aurora-deploy-{id}`) +- No changes to the deployment workflow or lifecycle +- No UI redesign (layout, styling, interactions stay the same) + +## Execution Order + +1. Alembic migration (rename tables & columns) +2. Backend models & imports +3. Backend schemas (Pydantic) +4. Backend utils (compilation) +5. Backend GraphQL (types, resolvers, schema wiring) +6. Backend seed script +7. Frontend GraphQL queries +8. Frontend components (rename files, update imports) +9. Frontend routes & navigation +10. Frontend i18n translations +11. CLAUDE.md documentation update diff --git a/docs/plans/2026-03-08-rename-contracts-to-services.md b/docs/plans/2026-03-08-rename-contracts-to-services.md new file mode 100644 index 0000000..3695c5c --- /dev/null +++ b/docs/plans/2026-03-08-rename-contracts-to-services.md @@ -0,0 +1,699 @@ +# Rename Executable Contracts to Services — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Rename the "executable contract" abstraction to "service" throughout the entire stack — DB, backend, GraphQL API, frontend, i18n, docs. + +**Architecture:** The underlying schema format, compilation logic, and deployment machinery stay the same. This is a naming/framing change. Tables are renamed via Alembic migration, Python/JS files are renamed and classes updated, GraphQL API surface changes, frontend routes change from `/app/contracts` to `/app/services`. + +**Tech Stack:** Alembic (migration), SQLAlchemy 1.4 (models), Strawberry (GraphQL), React + Apollo Client (frontend), i18next (translations) + +**Design doc:** `docs/plans/2026-03-08-rename-contracts-to-services-design.md` + +--- + +### Task 1: Alembic Migration — Rename Tables and Columns + +**Files:** +- Create: `backend/app/alembic/versions/_rename_contracts_to_services.py` + +**Context:** Latest alembic head is `b7c9e2f4a1d3`. The migration renames tables and columns only — no data changes, no type changes. + +**Step 1: Generate empty migration** + +Run: `docker-compose exec backend alembic revision --autogenerate -m "rename contracts to services"` + +Then replace the generated content with manual renames (autogenerate won't detect renames). + +**Step 2: Write the migration** + +```python +"""rename contracts to services + +Revision ID: +Revises: b7c9e2f4a1d3 +""" +from alembic import op + +revision = "" +down_revision = "b7c9e2f4a1d3" +branch_labels = None +depends_on = None + + +def upgrade(): + # Rename tables + op.rename_table("executable_contract", "service_definition") + op.rename_table("file_contract_binding", "service_binding") + + # Rename columns in service_definition + op.alter_column("service_definition", "contract_key", new_column_name="service_key") + op.alter_column("service_definition", "schema_json", new_column_name="config_json") + + # Rename columns in service_binding + op.alter_column("service_binding", "contract_id", new_column_name="service_id") + + # Rename columns in server_deployment + op.alter_column("server_deployment", "contract_id", new_column_name="service_id") + op.alter_column("server_deployment", "binding_id", new_column_name="service_binding_id") + + # Rename indexes + op.execute("ALTER INDEX IF EXISTS ix_executable_contract_id RENAME TO ix_service_definition_id") + op.execute("ALTER INDEX IF EXISTS ix_executable_contract_contract_key RENAME TO ix_service_definition_service_key") + op.execute("ALTER INDEX IF EXISTS ix_file_contract_binding_id RENAME TO ix_service_binding_id") + + # Rename constraints + op.execute(""" + ALTER TABLE service_definition + RENAME CONSTRAINT _executable_contract_contract_key_version_uc + TO _service_definition_service_key_version_uc + """) + op.execute(""" + ALTER TABLE service_binding + RENAME CONSTRAINT _file_contract_binding_file_id_contract_id_uc + TO _service_binding_file_id_service_id_uc + """) + + +def downgrade(): + # Reverse column renames + op.alter_column("server_deployment", "service_binding_id", new_column_name="binding_id") + op.alter_column("server_deployment", "service_id", new_column_name="contract_id") + op.alter_column("service_binding", "service_id", new_column_name="contract_id") + op.alter_column("service_definition", "config_json", new_column_name="schema_json") + op.alter_column("service_definition", "service_key", new_column_name="contract_key") + + # Reverse constraint renames + op.execute(""" + ALTER TABLE service_binding + RENAME CONSTRAINT _service_binding_file_id_service_id_uc + TO _file_contract_binding_file_id_contract_id_uc + """) + op.execute(""" + ALTER TABLE service_definition + RENAME CONSTRAINT _service_definition_service_key_version_uc + TO _executable_contract_contract_key_version_uc + """) + + # Reverse index renames + op.execute("ALTER INDEX IF EXISTS ix_service_binding_id RENAME TO ix_file_contract_binding_id") + op.execute("ALTER INDEX IF EXISTS ix_service_definition_service_key RENAME TO ix_executable_contract_contract_key") + op.execute("ALTER INDEX IF EXISTS ix_service_definition_id RENAME TO ix_executable_contract_id") + + # Reverse table renames + op.rename_table("service_binding", "file_contract_binding") + op.rename_table("service_definition", "executable_contract") +``` + +**Step 3: Verify migration runs** + +Run: `docker-compose exec backend alembic upgrade head` +Expected: Migration applies cleanly, tables renamed in DB. + +**Step 4: Verify downgrade works** + +Run: `docker-compose exec backend alembic downgrade -1` +Then: `docker-compose exec backend alembic upgrade head` +Expected: Both directions work. + +**Step 5: Commit** + +```bash +git add backend/app/alembic/versions/*rename_contracts_to_services* +git commit -m "feat: add migration to rename contracts to services" +``` + +--- + +### Task 2: Backend Models — Rename Files and Classes + +**Files:** +- Rename: `backend/app/db/models/executable_contract.py` → `backend/app/db/models/service_definition.py` +- Rename: `backend/app/db/models/file_contract_binding.py` → `backend/app/db/models/service_binding.py` +- Modify: `backend/app/db/models/server_deployment.py` +- Modify: `backend/app/db/models/__init__.py` +- Modify: `backend/app/db/models/file.py` + +**Step 1: Rename executable_contract.py → service_definition.py and update class** + +New file `service_definition.py` — rename class to `ServiceDefinition`, table to `service_definition`, column `contract_key` → `service_key`, column `schema_json` → `config_json`. Update constraint name. Update relationship back_populates. + +Key changes in the model: +- `__tablename__ = "service_definition"` +- `class ServiceDefinition(Base):` +- Column `service_key = Column(String, ...)` (was `contract_key`) +- Column `config_json = Column(MutableDict.as_mutable(JSON), ...)` (was `schema_json`) +- UniqueConstraint name: `_service_definition_service_key_version_uc` +- Relationship `file_bindings` → `bindings` with `back_populates="service"` +- Relationship `deployments` with `back_populates="service"` + +**Step 2: Rename file_contract_binding.py → service_binding.py and update class** + +New file `service_binding.py` — rename class to `ServiceBinding`, table to `service_binding`, column `contract_id` → `service_id`. Update constraint, relationships. + +Key changes: +- `__tablename__ = "service_binding"` +- `class ServiceBinding(Base):` +- Column `service_id = Column(Integer, ForeignKey("service_definition.id"), ...)` +- UniqueConstraint name: `_service_binding_file_id_service_id_uc` +- Relationship `service = relationship("ServiceDefinition", back_populates="bindings")` +- Relationship `file` stays, update back_populates if needed + +**Step 3: Update server_deployment.py** + +- `binding_id` → `service_binding_id` with FK to `service_binding.id` +- `contract_id` → `service_id` with FK to `service_definition.id` +- Relationship `binding` → `service_binding = relationship("ServiceBinding", ...)` +- Relationship `contract` → `service = relationship("ServiceDefinition", ...)` + +**Step 4: Update models/__init__.py** + +```python +from .service_definition import ServiceDefinition +from .service_binding import ServiceBinding +# Remove old imports: ExecutableContract, FileContractBinding +# Update __all__ list +``` + +**Step 5: Update file.py** + +- Relationship `contract_bindings` → `service_bindings` +- `back_populates` references updated + +**Step 6: Delete old files** + +Remove `executable_contract.py` and `file_contract_binding.py` from models dir. + +**Step 7: Verify imports work** + +Run: `docker-compose exec backend python3 -c "from app.db.models import ServiceDefinition, ServiceBinding; print('OK')"` +Expected: OK + +**Step 8: Commit** + +```bash +git add backend/app/db/models/ +git commit -m "refactor: rename contract models to service models" +``` + +--- + +### Task 3: Backend Schemas & Utils — Rename Pydantic Models and Compilation + +**Files:** +- Rename: `backend/app/db/schemas/executable_contract.py` → `backend/app/db/schemas/service_definition.py` +- Rename: `backend/app/utils/executable_contract.py` → `backend/app/utils/service_definition.py` + +**Step 1: Rename and update schema file** + +Rename `executable_contract.py` to `service_definition.py`. Update class names: +- `ExecutableContractAuthoringV1` → `ServiceDefinitionAuthoringV1` +- `ContractUI` → `ServiceUI` +- `ContractCompileError` stays in utils (rename there) + +**Important:** Do NOT change the internal JSON field names (`contractKey`, `exec`, `params`). These are part of the stored schema format. The Pydantic model field name stays `contractKey` — it's the JSON key, not a Python naming concern. + +Keep `ALLOWED_TEMPLATE_VARS = {"jobId", "contractKey", "paramKey"}` — these are template variables in stored JSON. + +**Step 2: Rename and update utils file** + +Rename `executable_contract.py` to `service_definition.py`. Update: +- `ContractCompileError` → `ServiceCompileError` +- `compile_executable_contract_preview()` → `compile_service_preview()` +- Update imports from new schema path + +**Step 3: Delete old files** + +Remove old `executable_contract.py` from both schemas/ and utils/. + +**Step 4: Verify** + +Run: `docker-compose exec backend python3 -c "from app.db.schemas.service_definition import ServiceDefinitionAuthoringV1; print('OK')"` +Run: `docker-compose exec backend python3 -c "from app.utils.service_definition import compile_service_preview; print('OK')"` +Expected: Both print OK + +**Step 5: Commit** + +```bash +git add backend/app/db/schemas/ backend/app/utils/ +git commit -m "refactor: rename contract schemas and utils to service" +``` + +--- + +### Task 4: Backend GraphQL — Rename Types and Resolvers + +**Files:** +- Rename: `backend/app/graphql/executable_contract.py` → `backend/app/graphql/service_definition.py` +- Modify: `backend/app/graphql/deployment.py` +- Modify: `backend/app/graphql/schema.py` + +**Step 1: Rename and update GraphQL service_definition.py** + +Rename file. Update all names: +- Import `ServiceDefinition as DBServiceDefinition` (was `ExecutableContract as DBExecutableContract`) +- Import `ServiceDefinitionAuthoringV1` (was `ExecutableContractAuthoringV1`) +- Import `compile_service_preview` (was `compile_executable_contract_preview`) +- Type class: `class ServiceDefinitionType:` (was `ExecutableContract`) +- Field: `service_key` (was `contract_key`) +- Field: `config_json` (was `schema_json`) +- Resolver: `get_service_definition` (was `get_executable_contract`) +- Resolver: `get_service_definitions` (was `get_executable_contracts`) +- Resolver: `get_paginated_service_definitions` (was `get_paginated_executable_contracts`) +- Mutation: `create_service_definition` (was `create_executable_contract`) +- Mutation: `update_service_definition` (was `update_executable_contract`) +- Mutation: `delete_service_definition` (was `delete_executable_contract`) +- Resolver: `compile_service_preview_resolver` (was `compile_executable_contract_preview_resolver`) +- Resolver: `compile_service_preview_by_id_resolver` (was `compile_executable_contract_preview_by_id_resolver`) +- Internal helper: `_compact_config_json` (was `_compact_schema_json`) +- Error message: `"Service definition '{id}' not found"` (was `"Executable contract..."`) + +**Step 2: Update deployment.py** + +- Import `ServiceBinding as DBServiceBinding` (was `FileContractBinding as DBFileContractBinding`) +- Import `ServiceDefinition as DBServiceDefinition` (was `ExecutableContract as DBExecutableContract`) +- Type class: `class ServiceBindingType:` (was `FileContractBinding`) +- Field: `service_id` (was `contract_id`) +- Resolver: `get_service_bindings` (was `get_file_contract_bindings`) +- Mutation: `create_service_binding` (was `create_file_contract_binding`) +- Mutation: `delete_service_binding` (was `delete_file_contract_binding`) +- Mutation: `deploy_service` (was `deploy_contract`) +- ServerDeployment field references: `service_binding_id`, `service_id` (were `binding_id`, `contract_id`) +- Method: `service_title()` (was `contract_title()`) +- Relationship access: `.service_binding` and `.service` (were `.binding` and `.contract`) + +**Step 3: Update schema.py** + +Update all imports and field wiring: +- Import from `service_definition` module instead of `executable_contract` +- Import `ServiceBindingType` instead of `FileContractBinding` +- Query fields: `service_definition`, `service_definitions`, `paginated_service_definitions`, `service_bindings` +- Mutation fields: `compile_service_preview`, `compile_service_preview_by_id`, `create_service_definition`, `update_service_definition`, `delete_service_definition`, `create_service_binding`, `delete_service_binding`, `deploy_service` + +**Step 4: Delete old file** + +Remove `backend/app/graphql/executable_contract.py`. + +**Step 5: Verify GraphQL schema generates** + +Run: `docker-compose exec backend python3 -c "from app.graphql.schema import schema; print(schema.as_str()[:200])"` +Expected: Schema prints without error. + +**Step 6: Commit** + +```bash +git add backend/app/graphql/ +git commit -m "refactor: rename contract GraphQL types to service" +``` + +--- + +### Task 5: Backend Tasks & Seed — Update Deployment Tasks and Seed Script + +**Files:** +- Modify: `backend/tasks/deployment.py` +- Rename: `backend/app/seed_contracts.py` → `backend/app/seed_services.py` + +**Step 1: Update tasks/deployment.py** + +- Import `ServiceBinding` instead of `FileContractBinding` +- Import `ServiceDefinition` instead of `ExecutableContract` +- Import `compile_service_preview` from `app.utils.service_definition` +- Update all variable references: `binding` → `service_binding` where it references the model +- Update `contract` → `service` where it references `ServiceDefinition` +- Update function calls from `compile_executable_contract_preview` to `compile_service_preview` + +**Step 2: Rename and update seed script** + +Rename `seed_contracts.py` to `seed_services.py`. Update: +- Docstring: "Seed built-in service definitions" +- Import: `from app.db.models import ServiceDefinition` +- Variable: `BUILTIN_SERVICES` (was `BUILTIN_CONTRACTS`) +- All references to `ExecutableContract` → `ServiceDefinition` +- Column references: `service_key` (was `contract_key`), `config_json` (was `schema_json`) + +**Important:** The JSON payload inside each service definition still uses `contractKey` as the JSON field name — do NOT change the stored JSON structure. + +**Step 3: Delete old seed file** + +Remove `backend/app/seed_contracts.py`. + +**Step 4: Verify seed runs** + +Run: `docker-compose exec backend python3 app/seed_services.py` +Expected: Seeds run without error. + +**Step 5: Commit** + +```bash +git add backend/tasks/deployment.py backend/app/seed_services.py +git rm backend/app/seed_contracts.py +git commit -m "refactor: rename contract references in tasks and seed script" +``` + +--- + +### Task 6: Backend Tests — Update Test File + +**Files:** +- Rename: `backend/tests/executable_contract_preview_test.py` → `backend/tests/service_preview_test.py` + +**Step 1: Rename and update test file** + +- Rename file to `service_preview_test.py` +- Update imports to use new module paths +- Update function references from `compile_executable_contract_preview` to `compile_service_preview` +- Update variable names as needed (test function names can stay descriptive of what they test) + +**Step 2: Run tests** + +Run: `docker-compose exec backend pytest tests/service_preview_test.py -v` +Expected: All tests pass. + +**Step 3: Commit** + +```bash +git add backend/tests/ +git rm backend/tests/executable_contract_preview_test.py +git commit -m "refactor: rename contract test to service test" +``` + +--- + +### Task 7: Frontend GraphQL Queries — Update Query Definitions + +**Files:** +- Modify: `frontend/src/features/contract-builder/constants.js` (will be moved in Task 8, but update content first) +- Modify: `frontend/src/queries/deployment.js` + +**Step 1: Update constants.js query names** + +All GraphQL operation names and field references must match the new backend API: +- `LIST_EXECUTABLE_CONTRACTS` → `LIST_SERVICE_DEFINITIONS` (query field: `paginatedServiceDefinitions` or `serviceDefinitions`) +- `CREATE_EXECUTABLE_CONTRACT` → `CREATE_SERVICE_DEFINITION` (mutation: `createServiceDefinition`) +- `UPDATE_EXECUTABLE_CONTRACT` → `UPDATE_SERVICE_DEFINITION` (mutation: `updateServiceDefinition`) +- `COMPILE_EXECUTABLE_CONTRACT_PREVIEW` → `COMPILE_SERVICE_PREVIEW` (mutation: `compileServicePreview`) +- `COMPILE_EXECUTABLE_CONTRACT_PREVIEW_BY_ID` → `COMPILE_SERVICE_PREVIEW_BY_ID` (mutation: `compileServicePreviewById`) +- `DEFAULT_CONTRACT_TEMPLATE` → `DEFAULT_SERVICE_TEMPLATE` + +**Step 2: Update deployment.js query names** + +- `GET_FILE_CONTRACT_BINDINGS` → `GET_SERVICE_BINDINGS` (query field: `serviceBindings`) +- `CREATE_FILE_CONTRACT_BINDING` → `CREATE_SERVICE_BINDING` (mutation: `createServiceBinding`) +- `DELETE_FILE_CONTRACT_BINDING` → `DELETE_SERVICE_BINDING` (mutation: `deleteServiceBinding`) +- `GET_CONTRACTS_FOR_BINDING` → `GET_SERVICES_FOR_BINDING` (query field: `serviceDefinitions`) +- `DEPLOY_CONTRACT` → `DEPLOY_SERVICE` (mutation: `deployService`) +- All field references: `contractId` → `serviceId`, `contractKey` → `serviceKey`, `schemaJson` → `configJson`, `bindingId` → `serviceBindingId` +- Note: `DEPLOY_EXECUTABLE` stays (it deploys via binding, name is fine). Update its `bindingId` param to `serviceBindingId` if the backend changed. + +**Step 3: Commit** + +```bash +git add frontend/src/features/contract-builder/constants.js frontend/src/queries/deployment.js +git commit -m "refactor: rename contract GraphQL queries to service" +``` + +--- + +### Task 8: Frontend Components — Rename Contract Builder to Service Editor + +**Files:** +- Rename directory: `frontend/src/features/contract-builder/` → `frontend/src/features/service-editor/` +- Rename: `ContractBuilderPage.jsx` → `ServiceEditorPage.jsx` +- Rename: `ContractListPage.jsx` → `ServiceListPage.jsx` +- Rename: `ParamBuilderPanel.jsx` → `ParamEditorPanel.jsx` +- Rename: `authoringAdapter.js` → `serviceAdapter.js` +- Modify: All other files in the directory (update imports) + +**Step 1: Rename directory** + +```bash +cd frontend/src/features +mv contract-builder service-editor +``` + +**Step 2: Rename files** + +```bash +cd service-editor +mv ContractBuilderPage.jsx ServiceEditorPage.jsx +mv ContractListPage.jsx ServiceListPage.jsx +mv ParamBuilderPanel.jsx ParamEditorPanel.jsx +mv authoringAdapter.js serviceAdapter.js +``` + +**Step 3: Update ServiceEditorPage.jsx** + +- Function name: `ServiceEditorPage` (was `ContractBuilderPage`) +- Import: `serviceDefinitionToDynamicSchema` from `./serviceAdapter` (was `authoringContractToDynamicSchema` from `./authoringAdapter`) +- Import: renamed query constants from `./constants` +- Variable names: `contracts` → `services`, `selectedContract` → `selectedService`, etc. +- Panel component imports: `ParamEditorPanel` (was `ParamBuilderPanel`) + +**Step 4: Update ServiceListPage.jsx** + +- Function name: `ServiceListPage` +- Import: renamed query constants +- Variable references updated + +**Step 5: Update ParamEditorPanel.jsx** + +- Function name: `ParamEditorPanel` +- Any "contract" labels in JSX → "service" + +**Step 6: Update serviceAdapter.js** + +- Function name: `serviceDefinitionToDynamicSchema` (was `authoringContractToDynamicSchema`) +- Internal references updated + +**Step 7: Update remaining files** + +- `AuthoringJsonPanel.jsx`: Update any "contract" text +- `FormPreviewPanel.jsx`: Update any "contract" references +- `CompileOutputPanel.jsx`: Update any "contract" references +- `constants.js`: Already updated in Task 7 +- `formUtils.js`, `builderUtils.js`: Update if they reference "contract" + +**Step 8: Update the directory index export** + +If there's an `index.js` or the directory default export points to the main page, update to export `ServiceEditorPage`. + +**Step 9: Commit** + +```bash +git add frontend/src/features/service-editor/ +git commit -m "refactor: rename contract-builder to service-editor" +``` + +--- + +### Task 9: Frontend Deployment — Update DeployModal and BindingModal + +**Files:** +- Modify: `frontend/src/features/deployment/DeployModal.jsx` +- Modify: `frontend/src/features/deployment/BindingModal.jsx` + +**Step 1: Update DeployModal.jsx** + +- Import from `../service-editor/serviceAdapter` (was `../contract-builder/authoringAdapter`) +- Import from `../service-editor/useDynamicForm` (was `../contract-builder/useDynamicForm`) +- Import renamed query constants from `../../queries/deployment` +- Variable renames: + - `selectedBindingId` → `selectedServiceBindingId` + - `selectedContractId` → `selectedServiceId` + - `selectedCatalogContractId` → `selectedCatalogServiceId` + - `contracts` → `services` + - `catalogContracts` → `catalogServices` + - `bindings` → `serviceBindings` +- Tab label: "App Catalog" → "Service Catalog" +- Function calls updated to use renamed mutations + +**Step 2: Update BindingModal.jsx** + +- Import renamed query constants +- Variable renames: `selectedContractId` → `selectedServiceId`, `contracts` → `services`, `bindings` → `serviceBindings` +- Modal title: "File-Contract Bindings" → "Service Bindings" +- Labels and references updated + +**Step 3: Commit** + +```bash +git add frontend/src/features/deployment/ +git commit -m "refactor: rename contract references in deployment UI" +``` + +--- + +### Task 10: Frontend Routes & App — Update Routing + +**Files:** +- Modify: `frontend/src/routes.js` +- Modify: `frontend/src/App.jsx` + +**Step 1: Update routes.js** + +```javascript +// Was: key: "contracts", path: "contracts", fullPath: "/app/contracts", labelKey: "Schemas" +{ + key: "services", + path: "services", + fullPath: "/app/services", + area: "app", + labelKey: "Services", + icon: /* keep same icon or update */, + permissions: ["admin", "ops"], + nav: true, +}, +// Was: key: "contractBuilder", path: "contracts/builder" +{ + key: "serviceEditor", + path: "services/editor", + fullPath: "/app/services/editor", + area: "app", + permissions: ["admin", "ops"], +}, +// Was: key: "contractBuilderById", path: "contracts/builder/:contractId" +{ + key: "serviceEditorById", + path: "services/editor/:serviceId", + fullPath: "/app/services/editor/:serviceId", + area: "app", + permissions: ["admin", "ops"], +}, +``` + +**Step 2: Update App.jsx** + +- Lazy import: `const ServiceEditorPage = lazy(() => import("./features/service-editor"));` +- Lazy import: `const ServiceListPage = lazy(() => import("./features/service-editor/ServiceListPage"));` +- Route elements: Update `routeMap.services.path`, `routeMap.serviceEditor.path`, `routeMap.serviceEditorById.path` +- Navigate default: `routeMap.serviceEditor.fullPath` + +**Step 3: Update any other navigation references** + +Search for `routeMap.contracts` or `routeMap.contractBuilder` in other components and update. + +**Step 4: Verify routes work** + +Run: `cd frontend && npm run build` +Expected: Build succeeds. + +**Step 5: Commit** + +```bash +git add frontend/src/routes.js frontend/src/App.jsx +git commit -m "refactor: rename contract routes to service routes" +``` + +--- + +### Task 11: Frontend i18n — Update Translations + +**Files:** +- Modify: `frontend/public/locales/en/translation.json` +- Modify: `frontend/public/locales/zh/translation.json` + +**Step 1: Update English translations** + +Key changes: +- `"Schemas"` → `"Services"` +- `"Command Schemas"` → `"Service Definitions"` +- `"Schema Builder"` → `"Service Editor"` +- `"Contracts"` → `"Services"` +- `"No contracts yet"` → `"No services yet"` +- `"Contract Key"` → `"Service Key"` +- `"Contract"` → `"Service"` +- `"File-Contract Bindings"` → `"Service Bindings"` +- `"App Catalog"` → `"Service Catalog"` +- `"Preview Auto Compiles By Stored Contract"` → `"Preview Auto Compiles By Stored Service"` +- Add any new keys needed + +**Step 2: Update Chinese translations** + +Mirror the English changes: +- `"方案"` stays (good word for "Service Definition" in this context) or update to `"服务"` +- `"方案列表"` → `"服务列表"` +- `"还没有方案"` → `"还没有服务"` +- `"方案标识"` → `"服务标识"` +- `"文件-合约绑定"` → `"服务绑定"` + +**Step 3: Commit** + +```bash +git add frontend/public/locales/ +git commit -m "refactor: rename contract i18n strings to service" +``` + +--- + +### Task 12: Documentation — Update CLAUDE.md and Memory + +**Files:** +- Modify: `CLAUDE.md` +- Modify: Memory files if needed + +**Step 1: Update CLAUDE.md** + +The "Executable Contract System" section (around line 109) needs to be renamed to "Service Definition System" with all internal references updated: +- "Executable Contract" → "Service Definition" +- "contract builder" → "service editor" +- File paths updated to new locations +- `authoringAdapter.js` → `serviceAdapter.js` +- `ContractBuilderPage.jsx` → `ServiceEditorPage.jsx` +- `features/contract-builder/` → `features/service-editor/` +- `deployContract` → `deployService` + +**Step 2: Update deploy/ submodule docs (optional)** + +If working within the deploy submodule: +- `deploy/executable-contract-schema.en.md` → rename or update title +- `deploy/executable-contract-schema.zh.md` → rename or update title + +This is optional and can be done separately since deploy/ is a submodule. + +**Step 3: Commit** + +```bash +git add CLAUDE.md +git commit -m "docs: update CLAUDE.md for service definition rename" +``` + +--- + +### Task 13: Integration Verification + +**Step 1: Run all backend tests** + +Run: `docker-compose exec backend pytest -v` +Expected: All tests pass. + +**Step 2: Build frontend** + +Run: `cd frontend && npm run build` +Expected: Build succeeds with no errors. + +**Step 3: Full stack smoke test** + +Run: `docker-compose up -d && docker-compose exec backend alembic upgrade head` + +Verify: +1. Navigate to `/app/services` — should show the service list +2. Navigate to `/app/services/editor` — should load the service editor +3. Open deploy modal — "Service Catalog" tab should work +4. Seed services: `docker-compose exec backend python3 app/seed_services.py` + +**Step 4: Final commit if any fixups needed** + +```bash +git add -A +git commit -m "fix: address integration issues from contract-to-service rename" +``` + +--- + +## Execution Notes + +- **Order matters**: Tasks 1-6 (backend) must complete before Tasks 7-11 (frontend), since the GraphQL API surface changes. +- **Tasks 7-11** (frontend) can potentially be parallelized since they touch different files, but Task 8 (directory rename) should come before Task 9 (DeployModal imports from the new path). +- **JSON schema format**: Never change `contractKey` or other field names inside stored JSON payloads. The Pydantic model field stays `contractKey` — only Python class names, table names, and column names change. +- **Submodule changes**: The `frontend/` directory is a git submodule. Commits to frontend files are commits within the submodule. diff --git a/docs/plans/2026-03-09-contextual-binding-panel-design.md b/docs/plans/2026-03-09-contextual-binding-panel-design.md new file mode 100644 index 0000000..c379a03 --- /dev/null +++ b/docs/plans/2026-03-09-contextual-binding-panel-design.md @@ -0,0 +1,36 @@ +# Contextual Binding Panel — Design + +**Date:** 2026-03-09 +**Status:** Approved +**Goal:** Replace the generic BindingModal with a contextual property panel that adapts based on whether it was opened from a file card or a service definition. + +--- + +## Core Concept + +The BindingModal becomes a small, focused modal that shows "the other side" — from a file you see services, from a service you see files. + +## From a File Card (fileId provided) + +- **Header:** File name + version as title (e.g. "gost-v3.0.2") +- **Body:** Simple list of bound services. Each row: service title, service key badge, version. Clicking a row selects it (visual highlight). +- **Footer area:** + - Inline dropdown of available services + "Add" button + - "Remove" button appears when a row is selected (with confirmation) + +## From a Service Definition (serviceId provided) + +- **Header:** Service title as modal title (e.g. "Gost Relay") +- **Body:** Simple list of bound files. Each row: file name, version, file size. Clicking a row selects it (visual highlight). +- **Footer area:** + - Inline dropdown of available executable files + "Add" button + - "Remove" button appears when a row is selected (with confirmation) + +## Shared Behavior + +- Small modal (`max-w-md`) +- Empty state: "No bindings yet" with add dropdown still visible +- The "known" side (file or service) is in the header, never in the list +- No table — clean list with subtle dividers +- Selected row gets `bg-primary/10` highlight + "Remove" button appears +- Confirmation before removing a binding diff --git a/docs/plans/2026-03-09-unified-deploy-service-design.md b/docs/plans/2026-03-09-unified-deploy-service-design.md new file mode 100644 index 0000000..332b752 --- /dev/null +++ b/docs/plans/2026-03-09-unified-deploy-service-design.md @@ -0,0 +1,65 @@ +# Unified Deploy Service UX — Design + +**Date:** 2026-03-09 +**Status:** Approved +**Goal:** Simplify the deployment UX by merging the "Service Catalog" and "From Binding" tabs into a single unified list of deployable items, rename "Deploy Executable" to "Deploy Service", and extract binding management into a standalone dialog. + +--- + +## 1. Deploy Modal Redesign + +**Title:** "Deploy Service" (was "Deploy Executable") + +**Step 1 — Pick a deployable item.** A single flat list (no tabs) with two kinds of entries: + +- **Built-in services** (`hasSource=true`): Show title, version, key badge, "Built-in" badge +- **Bindings** (file ↔ service pairs): Show as "filename → Service Title" with file type/version info + +Empty state: "No services available" with links to "Browse Service Definitions" and "Upload a File". + +**Step 2 — Configure & Deploy.** Same as today: port selector (if `requiresPort`), dynamic parameter form, deploy button. + +## 2. Binding Management Dialog + +A standalone reusable dialog for CRUD on file-to-service bindings. Accessible from two places: + +- **Service Definitions page** — "Manage Bindings" button on a service definition, dialog opens pre-filtered to that service +- **File Center** — "Manage Bindings" button on executable file cards, dialog opens pre-filtered to that file + +The dialog shows a table of bindings (file ↔ service), with create and delete actions. Same dialog component, different initial filter context. + +## 3. Unified Backend Mutation + +Merge `deployService` + `deployExecutable` into a single `deployService` mutation: + +```graphql +deployService( + serviceId: ID # for built-in (direct) + serviceBindingId: ID # for binding-based + serverIds: [ID!]! + values: JSON + portId: ID +): ServerDeployment +``` + +Exactly one of `serviceId` or `serviceBindingId` must be provided. The resolver dispatches to the same deployment logic either way. + +## 4. i18n Changes + +- "Deploy Executable" → "Deploy Service" (en + zh) +- Remove "Service Catalog" and "From Binding" tab keys +- Add empty state strings ("No services available", "Browse Service Definitions", "Upload a File") + +## 5. What Gets Removed + +- Tab UI in DeployModal (Service Catalog / From Binding) +- Inline binding creation inside the deploy modal +- `deployExecutable` mutation (replaced by unified `deployService`) +- `DEPLOY_EXECUTABLE` frontend mutation query + +## 6. What Stays the Same + +- Step 2 configure flow (port selector, dynamic form, deploy) +- Service definition schema and compilation +- Binding data model (file ↔ service M2M) +- `BindingModal` component (refactored into standalone reusable dialog) diff --git a/docs/plans/2026-03-09-unified-deploy-service.md b/docs/plans/2026-03-09-unified-deploy-service.md new file mode 100644 index 0000000..d15dbf9 --- /dev/null +++ b/docs/plans/2026-03-09-unified-deploy-service.md @@ -0,0 +1,833 @@ +# Unified Deploy Service UX — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Merge the "Service Catalog" and "From Binding" tabs into a single unified deployable list, rename "Deploy Executable" → "Deploy Service", unify backend mutations, and extract binding management into a standalone reusable dialog. + +**Architecture:** The DeployModal loses its tab UI and becomes a simple two-step flow: pick from a flat list (built-in services + bindings), then configure and deploy. The backend's two deploy mutations (`deployExecutable` + `deployService`) merge into one `deployService` that accepts either `serviceId` or `serviceBindingId`. The BindingModal gets `modalProps` support for pre-filtering by file or service, and entry points are added to ServiceListPage and FileCard. + +**Tech Stack:** React 18, Apollo Client (GraphQL), Strawberry (Python GraphQL), SQLAlchemy, DaisyUI 5, i18next + +--- + +## Task 1: Unify backend mutation — merge `deployExecutable` into `deployService` + +**Files:** +- Modify: `backend/app/graphql/deployment.py:286-366` (remove `deploy_executable_resolver`) +- Modify: `backend/app/graphql/deployment.py:369-448` (update `deploy_service_resolver` to accept optional `service_binding_id`) +- Modify: `backend/app/graphql/schema.py:211-219` (remove `deploy_executable` field, update `deploy_service` field) + +**Step 1: Update `deploy_service_resolver` to accept both `serviceId` and `serviceBindingId`** + +In `backend/app/graphql/deployment.py`, replace `deploy_service_resolver` (lines 369-448) with a unified version that handles both paths. The resolver accepts `service_id: Optional[int] = None` and `service_binding_id: Optional[int] = None`, validates exactly one is provided, then resolves the service definition from whichever path was given. + +```python +async def deploy_service_resolver( + info: Info, + server_ids: List[int], + values: JSON, + service_id: Optional[int] = None, + service_binding_id: Optional[int] = None, + port_id: Optional[int] = None, +) -> List[ServerDeployment]: + """Deploy a service to one or more servers. + + Exactly one of service_id or service_binding_id must be provided. + - service_id: deploy a built-in service directly (has source acquisition) + - service_binding_id: deploy via a file-to-service binding + """ + from tasks.deployment import deploy_executable_task + + if not service_id and not service_binding_id: + raise ValueError("Either serviceId or serviceBindingId must be provided") + if service_id and service_binding_id: + raise ValueError("Provide only one of serviceId or serviceBindingId, not both") + + user = info.context["request"].state.user + results = [] + pending_tasks = [] + + async with async_db_session() as db: + # Resolve service definition and validate + if service_binding_id: + binding = (await db.execute( + select(DBServiceBinding).where(DBServiceBinding.id == service_binding_id) + )).scalars().first() + if not binding: + raise ValueError(f"Service binding {service_binding_id} not found") + service_def = (await db.execute( + select(DBServiceDefinition).where(DBServiceDefinition.id == binding.service_id) + )).scalars().first() + if not service_def: + raise ValueError(f"Service definition for binding {service_binding_id} not found") + else: + service_def = (await db.execute( + select(DBServiceDefinition).where(DBServiceDefinition.id == service_id) + )).scalars().first() + if not service_def: + raise ValueError(f"Service definition {service_id} not found") + + requires_port = _parse_requires_port(service_def.config_json) + await _validate_port_for_deployment(db, port_id, server_ids, requires_port) + + for sid in server_ids: + # Upsert: find existing by (binding+server) or (service+server) + if service_binding_id: + existing_stmt = select(DBServerDeployment).where( + DBServerDeployment.service_binding_id == service_binding_id, + DBServerDeployment.server_id == sid, + ) + else: + existing_stmt = select(DBServerDeployment).where( + DBServerDeployment.service_id == service_id, + DBServerDeployment.server_id == sid, + ) + existing = (await db.execute(existing_stmt)).scalars().first() + + if existing: + existing.values_json = values + existing.status = DeploymentStatusEnum.PENDING + existing.is_active = True + existing.port_id = port_id + deployment = existing + else: + deployment = DBServerDeployment( + service_binding_id=service_binding_id, + service_id=service_id, + server_id=sid, + values_json=values, + status=DeploymentStatusEnum.PENDING, + port_id=port_id, + ) + db.add(deployment) + + await db.flush() + + log = DBDeploymentLog( + deployment_id=deployment.id, + action=DeploymentActionEnum.DEPLOY, + status=DeploymentLogStatusEnum.PENDING, + created_by_id=user.id if user else None, + ) + db.add(log) + await db.flush() + + pending_tasks.append((deployment.id, log.id, log)) + results.append(deployment) + + try: + await db.commit() + except IntegrityError: + raise ValueError("Port is already in use by another deployment") + + for dep_id, log_id, log in pending_tasks: + task_result = deploy_executable_task(dep_id, log_id) + log.task_id = task_result.id + await db.commit() + + for dep in results: + await db.refresh(dep) + + return results +``` + +**Step 2: Remove `deploy_executable_resolver`** + +Delete lines 286-366 (the old `deploy_executable_resolver` function) from `deployment.py`. + +**Step 3: Update schema.py — remove `deploy_executable`, keep unified `deploy_service`** + +In `backend/app/graphql/schema.py`: +- Remove the import of `deploy_executable_resolver` (line 34) +- Remove the `deploy_executable` field (lines 212-215) +- The `deploy_service` field (lines 216-219) stays as-is — strawberry auto-maps the new optional args + +**Step 4: Verify backend builds** + +Run: `docker-compose exec backend python -c "from app.graphql.schema import schema; print('OK')"` +Expected: `OK` + +**Step 5: Commit** + +```bash +git add backend/app/graphql/deployment.py backend/app/graphql/schema.py +git commit -m "refactor: unify deployExecutable + deployService into single mutation" +``` + +--- + +## Task 2: Update frontend GraphQL queries + +**Files:** +- Modify: `frontend/src/queries/deployment.js:135-181` (remove `DEPLOY_EXECUTABLE`, update `DEPLOY_SERVICE`) + +**Step 1: Update `DEPLOY_SERVICE` mutation to accept optional `serviceBindingId`** + +In `frontend/src/queries/deployment.js`, replace the `DEPLOY_SERVICE` mutation (lines 159-181) with: + +```javascript +export const DEPLOY_SERVICE = gql` + mutation DeployService( + $serviceId: Int + $serviceBindingId: Int + $serverIds: [Int!]! + $values: JSON! + $portId: Int + ) { + deployService( + serviceId: $serviceId + serviceBindingId: $serviceBindingId + serverIds: $serverIds + values: $values + portId: $portId + ) { + id + serviceBindingId + serviceId + serverId + portId + status + createdAt + updatedAt + } + } +`; +``` + +**Step 2: Remove `DEPLOY_EXECUTABLE` mutation** + +Delete lines 135-157 (the `DEPLOY_EXECUTABLE` constant). + +**Step 3: Commit** + +```bash +git add frontend/src/queries/deployment.js +git commit -m "refactor: remove DEPLOY_EXECUTABLE query, unify into DEPLOY_SERVICE" +``` + +--- + +## Task 3: Rewrite DeployModal — unified flat list, no tabs + +**Files:** +- Modify: `frontend/src/features/deployment/DeployModal.jsx` (full rewrite of step 1 UI) + +**Step 1: Rewrite DeployModal** + +Replace the entire content of `DeployModal.jsx`. Key changes: +- Remove tab state, `selectedFileId`, `selectedServiceId`, `handleCreateBindingAndContinue` +- Remove `CREATE_SERVICE_BINDING` and `DEPLOY_EXECUTABLE` imports +- Remove `GET_EXECUTABLE_FILES` import (no longer needed in deploy flow) +- Single state: `selectedItem` — either `{ type: "service", serviceId }` or `{ type: "binding", bindingId, service }` +- Step 1 renders a flat list combining `catalogServices` and `bindings` +- Step 2 unchanged (port selector + dynamic form + deploy) +- `handleDeploy` always calls `deployService` with appropriate args +- Empty state with links + +```jsx +import { useState, useMemo, useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { useQuery, useMutation } from "@apollo/client"; +import classNames from "classnames"; +import { Package, Link as LinkIcon } from "lucide-react"; +import DataLoading from "../DataLoading"; +import useDynamicForm from "../service-editor/useDynamicForm"; +import { serviceDefinitionToDynamicSchema } from "../service-editor/serviceAdapter"; +import { + GET_SERVICE_BINDINGS, + GET_SERVICES_FOR_BINDING, + GET_AVAILABLE_PORTS, + DEPLOY_SERVICE, +} from "../../queries/deployment"; +import ModalShell from "../ui/ModalShell"; + +const DeployModal = ({ modalProps, close, resolve }) => { + const { t } = useTranslation(); + const serverId = modalProps?.serverId; + + const [step, setStep] = useState(1); + const [selectedItem, setSelectedItem] = useState(null); + const [selectedPortId, setSelectedPortId] = useState(null); + const [formValues, setFormValues] = useState({}); + + // Queries + const { data: bindingsData, loading: bindingsLoading } = + useQuery(GET_SERVICE_BINDINGS); + const { data: servicesData, loading: servicesLoading } = + useQuery(GET_SERVICES_FOR_BINDING); + const { data: portsData, loading: portsLoading } = useQuery(GET_AVAILABLE_PORTS, { + variables: { serverId }, + skip: !serverId, + }); + + const [deployService, { loading: deploying }] = useMutation(DEPLOY_SERVICE); + + const bindings = bindingsData?.serviceBindings ?? []; + const services = servicesData?.serviceDefinitions ?? []; + + const catalogServices = useMemo( + () => services.filter((s) => s.hasSource), + [services] + ); + + // Build unified list: built-in services first, then bindings + const deployableItems = useMemo(() => { + const items = []; + for (const s of catalogServices) { + items.push({ + key: `service-${s.id}`, + type: "service", + serviceId: s.id, + service: s, + label: s.title, + sublabel: `${s.serviceKey} v${s.version}`, + isBuiltin: s.isBuiltin, + }); + } + for (const b of bindings) { + items.push({ + key: `binding-${b.id}`, + type: "binding", + bindingId: b.id, + service: b.service, + label: `${b.file?.name || `File #${b.fileId}`}`, + sublabel: b.service?.title || b.service?.serviceKey || `Service #${b.serviceId}`, + fileVersion: b.file?.version, + }); + } + return items; + }, [catalogServices, bindings]); + + const selectedService = selectedItem?.service ?? null; + + const formSchema = useMemo(() => { + if (!selectedService?.configJson) return null; + try { + return serviceDefinitionToDynamicSchema(selectedService.configJson); + } catch { + return null; + } + }, [selectedService]); + + const requiresPort = selectedService?.configJson?.requiresPort !== false; + + const handleValuesChange = useCallback((values) => { + setFormValues(values); + }, []); + + const handleFormSubmit = useCallback((values) => { + setFormValues(values); + }, []); + + const handleSelectItem = (item) => { + setSelectedItem(item); + setSelectedPortId(null); + setStep(2); + }; + + const handleDeploy = async () => { + if (!serverId || !selectedItem) return; + + const variables = { + serverIds: [serverId], + values: formValues, + portId: selectedPortId, + }; + + if (selectedItem.type === "service") { + variables.serviceId = selectedItem.serviceId; + } else { + variables.serviceBindingId = selectedItem.bindingId; + } + + await deployService({ variables }); + if (resolve) resolve(true); + close(); + }; + + const handleCancel = () => { + if (resolve) resolve(false); + close(); + }; + + const handleBack = () => { + setSelectedItem(null); + setSelectedPortId(null); + setFormValues({}); + setStep(1); + }; + + const isAnyLoading = bindingsLoading || servicesLoading; + + return ( + + {/* Steps indicator */} +
    +
  • = 1 && "step-primary")}> + {t("Select")} +
  • +
  • = 2 && "step-primary")}> + {t("Configure")} +
  • +
+ + {isAnyLoading ? ( +
+ +
+ ) : ( + <> + {/* Step 1: Pick a deployable item */} + {step === 1 && ( +
+ {deployableItems.length === 0 ? ( + + ) : ( +
+ {deployableItems.map((item) => ( +
handleSelectItem(item)} + > +
+ {item.type === "service" ? ( + + ) : ( + + )} + {item.label} + {item.type === "binding" && ( + <> + + {item.sublabel} + + )} + {item.type === "service" && ( + + {item.sublabel} + + )} + {item.isBuiltin && ( + + {t("Built-in")} + + )} +
+
+ {item.fileVersion && ( + v{item.fileVersion} + )} + {item.type === "service" && ( + v{item.service?.version} + )} +
+
+ ))} +
+ )} + +
+ +
+
+ )} + + {/* Step 2: Configure & deploy */} + {step === 2 && ( +
+ {/* Port selector */} + {requiresPort && ( +
+ + + {!portsLoading && + portsData?.availablePortsForDeployment?.length === 0 && ( +

+ {t("No available ports on this server")} +

+ )} +
+ )} + + {/* Service form */} + {formSchema && ( +
+

{t("Parameters")}

+
+ +
+
+ )} + + {/* Summary */} +
+

{t("Summary")}

+
+
+ {t("Service")}:{" "} + + {selectedService?.title || selectedService?.serviceKey} + +
+ {selectedItem?.type === "binding" && ( +
+ {t("File")}:{" "} + {selectedItem.label} +
+ )} + {Object.keys(formValues).length > 0 && ( +
+ {t("Parameters")}: +
+                        {JSON.stringify(formValues, null, 2)}
+                      
+
+ )} +
+
+ +
+ + +
+
+ )} + + )} +
+ ); +}; + +function ServiceValuesForm({ formSchema, onSubmit, onValuesChange }) { + const { form } = useDynamicForm({ + schema: formSchema, + onSubmit, + onValuesChange, + }); + return form; +} + +export default DeployModal; +``` + +**Step 2: Commit** + +```bash +git add frontend/src/features/deployment/DeployModal.jsx +git commit -m "refactor: DeployModal — unified flat list, no tabs" +``` + +--- + +## Task 4: Update BindingModal to support pre-filtering via `modalProps` + +**Files:** +- Modify: `frontend/src/features/deployment/BindingModal.jsx` + +**Step 1: Add `modalProps` support for `fileId` and `serviceId` pre-filtering** + +Update `BindingModal` to accept `modalProps` and pass filter variables to `GET_SERVICE_BINDINGS`. When opened from FileCard, it receives `{ fileId }`. When opened from ServiceListPage, it receives `{ serviceId }`. The create form pre-selects the relevant dropdown. + +```jsx +const BindingModal = ({ modalProps, close, resolve }) => { +``` + +Add to the query: +```jsx +const filterFileId = modalProps?.fileId ?? null; +const filterServiceId = modalProps?.serviceId ?? null; + +const { + data: bindingsData, + loading: bindingsLoading, + refetch, +} = useQuery(GET_SERVICE_BINDINGS, { + variables: { + ...(filterFileId && { fileId: filterFileId }), + ...(filterServiceId && { serviceId: filterServiceId }), + }, +}); +``` + +Initialize the create form dropdowns from `modalProps`: +```jsx +const [selectedFileId, setSelectedFileId] = useState(filterFileId ? String(filterFileId) : ""); +const [selectedServiceId, setSelectedServiceId] = useState(filterServiceId ? String(filterServiceId) : ""); +``` + +**Step 2: Commit** + +```bash +git add frontend/src/features/deployment/BindingModal.jsx +git commit -m "feat: BindingModal accepts modalProps for pre-filtering by file or service" +``` + +--- + +## Task 5: Add "Manage Bindings" button to FileCard (for EXECUTABLE files) + +**Files:** +- Modify: `frontend/src/features/file/FileCard.jsx:168-184` (add binding button) + +**Step 1: Add a "Bindings" button for EXECUTABLE file type** + +Import `LinkIcon` and add a conditional button in the actions section. When clicked, it opens the binding modal pre-filtered to that file. + +Add to imports: +```jsx +import { Link as LinkIcon } from "lucide-react"; +``` + +Note: `Link` is already potentially conflicting with react-router. Use `Link as LinkIcon` since the existing imports already show a rename pattern (`File as FileIcon`). + +In the actions div (lines 169-184), add a "Bindings" button between Delete and Preview/Download, visible only for EXECUTABLE files: + +```jsx +{/* Actions */} +
+ + {file.type === "EXECUTABLE" && ( + + )} + +
+``` + +**Step 2: Commit** + +```bash +git add frontend/src/features/file/FileCard.jsx +git commit -m "feat: add Bindings button to executable file cards" +``` + +--- + +## Task 6: Add "Manage Bindings" button to ServiceListPage + +**Files:** +- Modify: `frontend/src/features/service-editor/ServiceListPage.jsx:116-128` (add bindings button to actions column) + +**Step 1: Import `useModal` and add button** + +Add imports: +```jsx +import { Link as LinkIcon } from "lucide-react"; +import { useModal } from "../../atoms/modal"; +``` + +In the component, add: +```jsx +const { open } = useModal(); +``` + +In the actions `` (lines 116-128), add a "Bindings" button before the "Open" button: + +```jsx + + + + +``` + +**Step 2: Commit** + +```bash +git add frontend/src/features/service-editor/ServiceListPage.jsx +git commit -m "feat: add Bindings button to service definitions list" +``` + +--- + +## Task 7: Update i18n translation files + +**Files:** +- Modify: `frontend/public/locales/en/translation.json` +- Modify: `frontend/public/locales/zh/translation.json` + +**Step 1: Update English translations** + +Changes: +- `"Deploy Executable"` → `"Deploy Service"` (keep the key, change value — actually the key IS the value in i18next default ns, so change the key) +- Add: `"No services available"`, `"Browse Service Definitions"`, `"Upload a File"`, `"Bindings"` +- The keys `"Service Catalog"` and `"From Binding"` can stay (no harm, may be used elsewhere) or be removed for cleanliness + +Add these new keys to the English translation file: +```json +"Deploy Service": "Deploy Service", +"No services available": "No services available", +"Browse Service Definitions": "Browse Service Definitions", +"Upload a File": "Upload a File", +"Bindings": "Bindings" +``` + +Remove (or leave, low priority): +```json +"Deploy Executable": "Deploy Executable", +"Service Catalog": "Service Catalog", +"From Binding": "From Binding" +``` + +**Step 2: Update Chinese translations** + +Add equivalent keys: +```json +"Deploy Service": "部署服务", +"No services available": "暂无可用服务", +"Browse Service Definitions": "浏览服务定义", +"Upload a File": "上传文件", +"Bindings": "绑定" +``` + +Remove the old keys that are no longer used. + +**Step 3: Commit** + +```bash +git add frontend/public/locales/en/translation.json frontend/public/locales/zh/translation.json +git commit -m "i18n: update translations for unified deploy service UX" +``` + +--- + +## Task 8: Clean up unused imports and dead code + +**Files:** +- Modify: `frontend/src/queries/deployment.js` (verify `DEPLOY_EXECUTABLE` is gone, `GET_EXECUTABLE_FILES` may still be needed by BindingModal) +- Verify: `frontend/src/features/deployment/DeployModal.jsx` has no stale imports +- Verify: No other files import `DEPLOY_EXECUTABLE` + +**Step 1: Search for remaining references to `DEPLOY_EXECUTABLE`** + +Run: `grep -r "DEPLOY_EXECUTABLE" frontend/src/` + +If any files still import it, update them. + +**Step 2: Verify `GET_EXECUTABLE_FILES` is still used** + +It's imported by `BindingModal.jsx` — keep it in `deployment.js`. + +**Step 3: Commit if any changes** + +```bash +git add -u frontend/ +git commit -m "chore: clean up unused deployment imports" +``` + +--- + +## Task 9: Manual smoke test + +**Step 1: Start the dev environment** + +```bash +docker-compose up -d +docker-compose exec backend alembic upgrade head +cd frontend && npm run dev +``` + +**Step 2: Test the deploy flow** + +1. Navigate to a server's deployment page +2. Click "Deploy" — verify modal title is "Deploy Service" +3. Verify a flat list appears with built-in services (Package icon, Built-in badge) and bindings (Link icon, "file → service" format) +4. Click a built-in service → configure step → deploy → verify deployment created +5. Click a binding → configure step → deploy → verify deployment created +6. Test empty state (if no services/bindings exist) + +**Step 3: Test binding management** + +1. Go to File Center → click "Bindings" on an executable file card → verify BindingModal opens pre-filtered to that file +2. Go to Service Definitions → click "Bindings" on a service row → verify BindingModal opens pre-filtered to that service +3. Create a binding, delete a binding — verify CRUD works + +**Step 4: Commit all changes if any fixes needed** + +```bash +git add -u +git commit -m "fix: smoke test fixes for unified deploy service" +``` diff --git a/docs/superpowers/plans/2026-03-24-design-system-foundation.md b/docs/superpowers/plans/2026-03-24-design-system-foundation.md new file mode 100644 index 0000000..2550979 --- /dev/null +++ b/docs/superpowers/plans/2026-03-24-design-system-foundation.md @@ -0,0 +1,764 @@ +# Design System Foundation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Establish radix-nova design language (font, bg-muted surface, shared patterns) and redesign the Servers page as proof-of-concept. + +**Architecture:** Install Noto Sans font, apply bg-muted page surface, update shared UI patterns (PageHeader, EmptyState, ThemedSuspense), then rewrite ServerList as a table-in-card with inline Progress bars for metrics. Delete unused card/stat sub-components. + +**Tech Stack:** React 19, Tailwind CSS 4, shadcn/ui radix-nova, Recharts, Apollo Client + +**Spec:** `docs/superpowers/specs/2026-03-24-design-system-foundation-design.md` + +--- + +## File Map + +### Modified files +- `frontend/src/index.css` — Font import + `--font-sans` variable +- `frontend/src/Layout.tsx:262-267` — `bg-muted`, `max-w-7xl`, `p-5 md:p-6` +- `frontend/src/features/ui/PageHeader.tsx` — Simplified to smaller typography +- `frontend/src/features/ui/EmptyState.tsx` — Rewrite to use `Empty` component +- `frontend/src/features/ThemedSuspense.tsx` — Centered `Spinner` on `bg-muted` +- `frontend/src/features/server/ServerList.tsx` — Full rewrite as table-in-card +- `frontend/src/features/server/ServerInfoModal.tsx` — Restyle to Field/FieldGroup pattern +- `frontend/src/features/server/ServerContainer.tsx:9` — Fix React Router v6 matchPath +- `frontend/src/features/file/FileCenter.tsx` — Rewrite as table-in-card +- `frontend/src/features/deployment/DeploymentList.tsx` — Rewrite as table-in-card + +### Deleted files +- `frontend/src/features/server/ServerCard.tsx` +- `frontend/src/features/server/ServerRow.tsx` +- `frontend/src/features/server/ServerStat.tsx` +- `frontend/src/features/server/ServerPortsStat.tsx` +- `frontend/src/features/server/ServerTrafficStat.tsx` +- `frontend/src/features/server/ServerSSHStat.tsx` +- `frontend/src/hooks/useServerItem.ts` +- `frontend/src/features/file/FileCard.tsx` +- `frontend/src/features/file/FileRow.tsx` + +--- + +## Task 1: Install font and update global styles + +**Files:** +- Modify: `frontend/package.json` +- Modify: `frontend/src/index.css` + +- [ ] **Step 1: Install Noto Sans font package** + +```bash +cd /home/lei/workspace/created/aurora/frontend && npm install @fontsource-variable/noto-sans +``` + +- [ ] **Step 2: Add font import to `index.css`** + +In `frontend/src/index.css`, add the font import after the existing imports (after line 3 `@import "shadcn/tailwind.css";`): + +```css +@import "@fontsource-variable/noto-sans"; +``` + +- [ ] **Step 3: Update `--font-sans` in the `@theme inline` block** + +Replace line 7: +```diff +- --font-sans: var(--font-sans); ++ --font-sans: 'Noto Sans Variable', sans-serif; +``` + +Leave `--font-heading: var(--font-sans);` as-is — it correctly inherits the new font. + +- [ ] **Step 4: Type-check** + +```bash +cd /home/lei/workspace/created/aurora/frontend && npx tsc --noEmit +``` + +- [ ] **Step 5: Commit (inside frontend submodule)** + +```bash +cd /home/lei/workspace/created/aurora/frontend +git add package.json package-lock.json src/index.css +git commit -m "style: add Noto Sans Variable font and update --font-sans theme token" +``` + +--- + +## Task 2: Update Layout — bg-muted surface and container + +**Files:** +- Modify: `frontend/src/Layout.tsx:262-267` + +- [ ] **Step 1: Add `bg-muted` to content area and update container** + +In `frontend/src/Layout.tsx`, find the content wrapper (around line 262): + +```diff +-
++
+``` + +And update the container div: + +```diff +-
++
+``` + +- [ ] **Step 2: Commit** + +```bash +cd /home/lei/workspace/created/aurora/frontend +git add src/Layout.tsx +git commit -m "style: apply bg-muted page surface and max-w-7xl container" +``` + +--- + +## Task 3: Update shared UI patterns + +**Files:** +- Modify: `frontend/src/features/ui/PageHeader.tsx` +- Modify: `frontend/src/features/ui/EmptyState.tsx` +- Modify: `frontend/src/features/ThemedSuspense.tsx` + +- [ ] **Step 1: Rewrite `PageHeader.tsx`** + +Read the current file first. Replace it with a simplified version that is backward-compatible (keeps `onAdd`/`addLabel` for existing callers, adds `children` for new callers): + +```tsx +import { Plus } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; + +interface PageHeaderProps { + title: string; + description?: string; + /** New: pass action buttons as children */ + children?: React.ReactNode; + /** Legacy: renders a "+ label" button */ + onAdd?: () => void; + /** Legacy: label for the add button */ + addLabel?: string; + className?: string; +} + +export function PageHeader({ title, description, children, onAdd, addLabel, className }: PageHeaderProps) { + return ( +
+
+

{title}

+ {description && ( +

{description}

+ )} +
+
+ {children} + {onAdd && ( + + )} +
+
+ ); +} + +export default PageHeader; +``` + +Key changes: title goes from `text-2xl font-extrabold` to `text-lg font-semibold`. Keeps `onAdd`/`addLabel` for backward compat with FileCenter and ServerPorts (out of scope). New code uses `children` pattern. No Card wrapping. + +- [ ] **Step 2: Rewrite `EmptyState.tsx`** + +Read the current file first. Replace it to use the `Empty` component. Keep backward-compatible props (`icon`, `action`, `description`) for callers outside scope (FileCenter, PortUsersCard): + +```tsx +import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent } from "@/components/ui/empty"; + +interface EmptyStateProps { + title: string; + /** Primary description text */ + message?: string; + /** Legacy alias for message */ + description?: string; + /** Legacy: icon element rendered above the title */ + icon?: React.ReactNode; + /** Legacy: action element rendered below the description */ + action?: React.ReactNode; + /** New: pass action buttons as children */ + children?: React.ReactNode; + className?: string; +} + +export function EmptyState({ title, message, description, icon, action, children, className }: EmptyStateProps) { + const desc = message || description; + return ( + + {icon && {icon}} + + {title} + {desc && {desc}} + + {(children || action) && ( + {children || action} + )} + + ); +} + +export default EmptyState; +``` + +- [ ] **Step 3: Update `ThemedSuspense.tsx`** + +Read the current file first. Replace with centered Spinner on bg-muted: + +```tsx +import { Spinner } from "@/components/ui/spinner"; + +export default function ThemedSuspense() { + return ( +
+ +
+ ); +} +``` + +- [ ] **Step 4: Type-check** + +```bash +cd /home/lei/workspace/created/aurora/frontend && npx tsc --noEmit 2>&1 | head -20 +``` + +Note: There may be type errors in files that consume the old PageHeader/EmptyState APIs (e.g., `onAdd` prop removed from PageHeader). Check which callers break and update them if they are in the server feature area (in scope). For callers in other pages (files, services, etc.), either keep backward compatibility by accepting both prop styles, or leave the errors for subsequent sub-projects. + +If backward compatibility is needed, add optional `onAdd`/`addLabel` props to PageHeader that render a Button with Plus icon when provided (preserving the old API while also supporting children). + +- [ ] **Step 5: Commit** + +```bash +cd /home/lei/workspace/created/aurora/frontend +git add src/features/ui/PageHeader.tsx src/features/ui/EmptyState.tsx src/features/ThemedSuspense.tsx +git commit -m "style: update PageHeader, EmptyState, and ThemedSuspense to radix-nova patterns" +``` + +--- + +## Task 4: Fix ServerContainer matchPath + +**Files:** +- Modify: `frontend/src/features/server/ServerContainer.tsx` + +- [ ] **Step 1: Read the file and fix matchPath call** + +The file uses React Router v5 `matchPath` API. Fix to v6: + +```diff +- const isServerListRoot = matchPath({ path: "/app/servers", exact: true }, location.pathname); ++ const isServerListRoot = matchPath("/app/servers", location.pathname); +``` + +Or if the pattern uses different syntax, read the file first and adjust accordingly. + +- [ ] **Step 2: Commit** + +```bash +cd /home/lei/workspace/created/aurora/frontend +git add src/features/server/ServerContainer.tsx +git commit -m "fix: update ServerContainer to use React Router v6 matchPath API" +``` + +--- + +## Task 5: Rewrite ServerList as table-in-card + +**Files:** +- Rewrite: `frontend/src/features/server/ServerList.tsx` +- Delete: `frontend/src/features/server/ServerCard.tsx` +- Delete: `frontend/src/features/server/ServerRow.tsx` +- Delete: `frontend/src/features/server/ServerStat.tsx` +- Delete: `frontend/src/features/server/ServerPortsStat.tsx` +- Delete: `frontend/src/features/server/ServerTrafficStat.tsx` +- Delete: `frontend/src/features/server/ServerSSHStat.tsx` +- Delete: `frontend/src/hooks/useServerItem.ts` + +This is the largest task. Read ALL files being deleted first to understand the logic that needs to be preserved. + +- [ ] **Step 1: Read all server component files** + +Read these files to understand the data flow and logic: +- `ServerList.tsx` — The parent. Has GraphQL query for paginated servers, subscription for metrics, state for modal. +- `ServerCard.tsx` — Renders a single server as a card. +- `ServerRow.tsx` — Renders a single server as a table row. +- `ServerStat.tsx` — Renders CPU/Mem/Network sparkline charts using `useServerMetrics` hook. +- `ServerPortsStat.tsx` — Renders `usedPorts/totalPorts`. +- `ServerSSHStat.tsx` — Subscribes to `CONNECT_SERVER_SUBSCRIPTION` for SSH status. +- `ServerTrafficStat.tsx` — Renders upload/download traffic. +- `useServerItem.ts` — Hook managing SSH connected state and edit handler. + +Also read the GraphQL queries at `src/queries/server.ts`. + +- [ ] **Step 2: Rewrite `ServerList.tsx`** + +The new ServerList should be a single table-in-card. Key structure: + +```tsx +// Imports: React, translation, Apollo, UI components +// Keep: GET_SERVERS_QUERY, SERVER_METRIC_SUBSCRIPTION, metricsMap state +// Keep: ServerInfoModal with local state (infoModalOpen, selectedServerId) +// Remove: framer-motion, ServerCard, ServerRow, listStyle toggle +// Remove: useServerItem import + + + + + +{/* Use gap-5 / mt-5 between page sections — the design system spacing convention */} + + {loading ? ( + + {/* Skeleton table rows */} + + ) : !servers?.length ? ( + + + + + + ) : ( + <> + + + + + Server + SSH + Ports + Traffic + CPU + Mem + Disk + Actions + + + + {servers.map((server) => { + const metric = metricsMap.get(server.id); + return ( + + {/* Server: name + address */} + +
{server.name || server.address}
+
{server.address}
+
+ {/* SSH: colored dot badge */} + + + + {label} + + + {/* Ports — GraphQL fields are portUsed and portTotal */} + + {server.portUsed ?? "—"} / {server.portTotal ?? "—"} + + {/* Traffic */} + +
↑ {formatBytes(metric?.upload)}
+
↓ {formatBytes(metric?.download)}
+
+ {/* CPU */} + +
+ + {metric?.cpu ?? 0}% +
+
+ {/* Mem — same pattern */} + {/* Disk — same pattern */} + {/* Actions */} + + + ... + +
+ ); + })} +
+
+
+ + {count} {t("servers")} + + + + )} +
+ + +``` + +**Data sources to preserve from old code:** +- `useQuery(GET_SERVERS_QUERY, { variables: { limit, offset } })` — paginated server list +- `client.subscribe({ query: SERVER_METRIC_SUBSCRIPTION })` — real-time metrics stored in `metricsMap` +- The `metricsMap` is a `Record` (keyed by numeric `serverId`) built from subscription events. Preserve the exact type from the old code — do NOT use `Map` as the serverId is a number. +- Ensure `fsRootUsedPct` from the subscription is captured in the MetricData and used for the Disk column's Progress bar. + +**SSH status logic:** The old `useServerItem` derives SSH connected state from `lastSeen` timestamp (10 min threshold) and metric `isOnline`. The old `ServerSSHStat` had an active probe via `CONNECT_SERVER_SUBSCRIPTION` — this is intentionally simplified to passive-only in the table view (the active probe/retry can be accessed from the server detail page). Inline the passive derivation as a helper function: + +```tsx +function getSshStatus(server: Server, metric?: MetricData): "connected" | "error" | "unknown" { + if (metric?.isOnline) return "connected"; + if (server.lastSeen) { + const tenMinAgo = Date.now() - 10 * 60 * 1000; + if (new Date(server.lastSeen).getTime() > tenMinAgo) return "connected"; + } + return "unknown"; +} +``` + +**Byte formatting:** Port the existing `readableSize` utility or create a simple inline helper: + +```tsx +function formatBytes(bytes?: number): string { + if (!bytes || bytes === 0) return "0 B"; + const units = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return `${(bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0)} ${units[i]}`; +} +``` + +- [ ] **Step 3: Delete old server components** + +```bash +cd /home/lei/workspace/created/aurora/frontend +rm src/features/server/ServerCard.tsx +rm src/features/server/ServerRow.tsx +rm src/features/server/ServerStat.tsx +rm src/features/server/ServerPortsStat.tsx +rm src/features/server/ServerTrafficStat.tsx +rm src/features/server/ServerSSHStat.tsx +rm src/hooks/useServerItem.ts +``` + +- [ ] **Step 4: Type-check** + +```bash +cd /home/lei/workspace/created/aurora/frontend && npx tsc --noEmit +``` + +Expected: Zero errors. If errors appear from other pages importing deleted components, those pages are out of scope — but the deleted components should ONLY be imported by ServerList (confirmed by the explore agent). + +- [ ] **Step 5: Commit** + +```bash +cd /home/lei/workspace/created/aurora/frontend +git add src/features/server/ServerList.tsx +git rm src/features/server/ServerCard.tsx src/features/server/ServerRow.tsx src/features/server/ServerStat.tsx src/features/server/ServerPortsStat.tsx src/features/server/ServerTrafficStat.tsx src/features/server/ServerSSHStat.tsx src/hooks/useServerItem.ts +git commit -m "feat: redesign Servers page as table-in-card with Progress bars and SSH dot indicators" +``` + +--- + +## Task 6: Restyle ServerInfoModal to FormDialog pattern + +**Files:** +- Modify: `frontend/src/features/server/ServerInfoModal.tsx` + +- [ ] **Step 1: Read the current file** + +Read the full `ServerInfoModal.tsx` to understand its form structure, tabs, and field layout. + +- [ ] **Step 2: Restyle form fields** + +Replace raw `Label` + `Input` pairs with `Field` + `FieldLabel` + `Input` from the radix-nova components: + +```diff +- +- ++ ++ Name ++ ++ +``` + +Wrap groups of fields in ``: + +```tsx +import { Field, FieldGroup, FieldLabel } from "@/components/ui/field" +``` + +Use `InputGroup` where appropriate, e.g., for address:port: + +```tsx +import { InputGroup, InputGroupInput, InputGroupAddon, InputGroupText } from "@/components/ui/input-group" + + + Address + + + + :{port} + + + +``` + +- [ ] **Step 3: Type-check** + +```bash +cd /home/lei/workspace/created/aurora/frontend && npx tsc --noEmit +``` + +- [ ] **Step 4: Commit** + +```bash +cd /home/lei/workspace/created/aurora/frontend +git add src/features/server/ServerInfoModal.tsx +git commit -m "style: restyle ServerInfoModal with Field/FieldGroup/InputGroup pattern" +``` + +--- + +## Task 7: Redesign FileCenter as table-in-card + +**Files:** +- Rewrite: `frontend/src/features/file/FileCenter.tsx` +- Delete: `frontend/src/features/file/FileCard.tsx` +- Delete: `frontend/src/features/file/FileRow.tsx` + +- [ ] **Step 1: Read all file feature files** + +Read `FileCenter.tsx`, `FileCard.tsx`, `FileRow.tsx`, `FileModal.tsx`, `FilePreviewModal.tsx` to understand the data flow, modals, and actions per file row (preview, download, bindings, delete). + +Also read `src/queries/file.ts` for the GraphQL queries. + +- [ ] **Step 2: Rewrite `FileCenter.tsx` as table-in-card** + +Same pattern as ServerList. Structure: + +```tsx + + + + + + {loading ? ( + {/* Skeleton rows */} + ) : !files?.length ? ( + + + + ) : ( + <> + + + + + Name + Type + Size + Version + Updated + Actions + + + + {files.map((file) => ( + + +
+ {/* File type icon */} + {file.name} +
+
+ + + {file.type} + + + {formatSize(file.size)} + {file.version} + {formatDate(file.updatedAt)} + + {/* Bindings button, Download button, DropdownMenu with Preview/Delete */} + +
+ ))} +
+
+
+ + {count} {t("total")} + + + + )} +
+ +{/* Modals rendered inline */} + + +``` + +**Key changes:** +- Remove `FileCard` and `FileRow` — inline the table rows directly +- Fold the per-row actions (preview, download, bindings, delete) into a `DropdownMenu` + dedicated action buttons +- Fold the `BindingModal` state from the old `FileCard`/`FileRow` into `FileCenter` +- The delete confirmation `AlertDialog` moves inline per-row or into the dropdown +- Port formatting helpers from old code (`readableSize`, date formatting) + +- [ ] **Step 3: Delete old file components** + +```bash +cd /home/lei/workspace/created/aurora/frontend +rm src/features/file/FileCard.tsx +rm src/features/file/FileRow.tsx +``` + +- [ ] **Step 4: Type-check** + +```bash +cd /home/lei/workspace/created/aurora/frontend && npx tsc --noEmit +``` + +- [ ] **Step 5: Commit** + +```bash +cd /home/lei/workspace/created/aurora/frontend +git add src/features/file/FileCenter.tsx +git rm src/features/file/FileCard.tsx src/features/file/FileRow.tsx +git commit -m "feat: redesign FileCenter as table-in-card with inline actions" +``` + +--- + +## Task 8: Redesign DeploymentList as table-in-card + +**Files:** +- Rewrite: `frontend/src/features/deployment/DeploymentList.tsx` + +- [ ] **Step 1: Read the current file** + +Read `DeploymentList.tsx`, `DeploymentStatusBadge.tsx`, `DeployModal.tsx`, `DeploymentDetailModal.tsx` to understand the data flow. + +Also read `src/queries/deployment.ts` for the GraphQL queries. + +- [ ] **Step 2: Rewrite `DeploymentList.tsx` as table-in-card** + +Same pattern. Structure: + +```tsx + + + + + + + + + {loading ? ( + {/* Skeleton rows */} + ) : !deployments?.length ? ( + + + + ) : ( + <> + + + + + Service + Status + Port + Created + Actions + + + + {deployments.map((deployment) => ( + + {deployment.serviceTitle || deployment.bindingTitle} + + {deployment.port?.portNumber ?? "—"} + {formatDate(deployment.createdAt)} + + + + + ))} + +
+
+ + {count} {t("total")} + + + + )} +
+ +{/* Modals */} + + +``` + +**Key changes:** +- Replace `PageSection` wrapper with the table-in-card `Card` pattern +- Replace `PageHeader` `onAdd` usage with `children` pattern +- Keep `DeploymentStatusBadge` as-is (already clean) +- Keep the existing modal state management (already uses inline Dialog from the migration) +- Port the Back/Ports/Refresh navigation buttons from the current implementation + +- [ ] **Step 3: Type-check** + +```bash +cd /home/lei/workspace/created/aurora/frontend && npx tsc --noEmit +``` + +- [ ] **Step 4: Commit** + +```bash +cd /home/lei/workspace/created/aurora/frontend +git add src/features/deployment/DeploymentList.tsx +git commit -m "feat: redesign DeploymentList as table-in-card" +``` + +--- + +## Task 9: Visual verification + +- [ ] **Step 1: Ensure docker stack is running** + +```bash +cd /home/lei/workspace/created/aurora && docker compose restart frontend +``` + +Wait for Vite to start. + +- [ ] **Step 2: Browse and screenshot key pages** + +Using agent-browser or manual browser: + +1. Navigate to `http://aurora.localhost:8060/app/servers` — verify table-in-card layout, Progress bars, SSH dots, bg-muted background +2. Navigate to `http://aurora.localhost:8060/app/files` — verify table-in-card layout, file type badges, action buttons +3. Click into a server → verify deployment table-in-card, status badges, Deploy button +4. Open the "Add Server" dialog — verify Field/FieldGroup form pattern +5. Check that the sidebar and navbar are unaffected + +- [ ] **Step 3: Fix any visual issues found** + +Address spacing, alignment, or rendering problems found during verification. + +- [ ] **Step 4: Final commit if fixes were needed** + +```bash +cd /home/lei/workspace/created/aurora/frontend +git add -A src/ +git commit -m "fix: visual polish from design verification pass" +``` diff --git a/docs/superpowers/plans/2026-03-24-radix-nova-migration.md b/docs/superpowers/plans/2026-03-24-radix-nova-migration.md new file mode 100644 index 0000000..6993c61 --- /dev/null +++ b/docs/superpowers/plans/2026-03-24-radix-nova-migration.md @@ -0,0 +1,980 @@ +# Radix-Nova UI Migration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace all existing shadcn/ui components with radix-nova style, rebuild layout on shadcn Sidebar, replace Jotai modal manager with inline dialogs, switch to Sonner toasts, and migrate all pages. + +**Architecture:** Drop-and-replace of 58 radix-nova components, then wave-based page migration. Layout rebuilt with shadcn Sidebar + sticky navbar in SidebarInset. Modal pattern shifts from centralized Jotai atom-based ModalManager to local `useState` + Dialog primitives per page. + +**Tech Stack:** React 19, Vite 7, Tailwind CSS 4, shadcn/ui radix-nova style, Radix UI, Sonner, Jotai, Apollo Client, React Router 6 + +**Spec:** `docs/superpowers/specs/2026-03-24-radix-nova-migration-design.md` + +**Zip source:** `/tmp/newui/` (extracted from `/tmp/b_v8Z7GKXAjyF-1774381622427.zip`) + +--- + +## File Map + +### New files (from zip) +- `frontend/src/components/ui/*.tsx` — 56 radix-nova components (replacing existing 20) +- `frontend/src/hooks/use-mobile.ts` — mobile detection hook (used by sidebar) + +### Major rewrites +- `frontend/src/index.css` — New zinc light/dark theme, stripped custom fonts +- `frontend/src/Layout.tsx` — Rebuilt with shadcn Sidebar + sticky navbar +- `frontend/src/App.tsx` — Remove ModalManager and Notification renders + +### Files deleted +- `frontend/src/features/layout/SideBar.tsx` +- `frontend/src/features/layout/NavBar.tsx` +- `frontend/src/features/modal/ModalManager.tsx` +- `frontend/src/features/modal/ConfirmationModal.tsx` +- `frontend/src/features/ui/ModalShell.tsx` +- `frontend/src/features/Notification.tsx` +- `frontend/src/atoms/notification.ts` +- `frontend/src/atoms/notificationManager.ts` +- `frontend/src/atoms/modal.ts` +- `frontend/src/atoms/layout.ts` +- `frontend/src/plugins/tailwind-mix.js` + +### Files modified across waves +- All page components in `frontend/src/features/` — updated imports, variant props, modal patterns +- `frontend/src/graphql.ts` — `notify()` → `toast()` +- `frontend/src/main.tsx` — Add Sonner `` +- `frontend/src/features/ui/PageHeader.tsx`, `PageSection.tsx`, `EmptyState.tsx` — new primitives +- `frontend/src/features/DataLoading.tsx`, `Paginator.tsx`, `ThemedSuspense.tsx` — updated + +--- + +## Task 1: Install new dependencies + +**Files:** +- Modify: `frontend/package.json` + +- [ ] **Step 1: Install new packages** + +```bash +cd frontend && npm install sonner vaul embla-carousel-react react-day-picker date-fns input-otp react-resizable-panels +``` + +- [ ] **Step 2: Verify install succeeded** + +```bash +cd frontend && npm ls sonner vaul embla-carousel-react react-day-picker input-otp react-resizable-panels +``` + +Expected: All packages listed without errors. + +- [ ] **Step 3: Commit** + +```bash +git add frontend/package.json frontend/package-lock.json +git commit -m "chore: add radix-nova component dependencies (sonner, vaul, embla, etc.)" +``` + +--- + +## Task 2: Drop-replace all UI components + +**Files:** +- Replace: `frontend/src/components/ui/*.tsx` (20 files replaced, 36 new files added) +- Create: `frontend/src/hooks/use-mobile.ts` + +- [ ] **Step 1: Clear existing components and copy radix-nova set** + +```bash +# Remove existing UI components +rm frontend/src/components/ui/*.tsx + +# Copy all radix-nova components EXCEPT toast.tsx, toaster.tsx, use-mobile.tsx, use-toast.ts +for f in /tmp/newui/components/ui/*.tsx; do + base=$(basename "$f") + case "$base" in + toast.tsx|toaster.tsx|use-mobile.tsx) continue ;; + *) cp "$f" frontend/src/components/ui/ ;; + esac +done + +# Do NOT copy use-toast.ts from components/ui/ +# Copy the hooks version of use-mobile +cp /tmp/newui/hooks/use-mobile.ts frontend/src/hooks/use-mobile.ts +``` + +- [ ] **Step 2: Strip `"use client"` directives from all copied files** + +```bash +cd frontend && sed -i '/^"use client"$/d' src/components/ui/*.tsx +``` + +- [ ] **Step 3: Verify file count** + +```bash +ls frontend/src/components/ui/*.tsx | wc -l +``` + +Expected: 56 files. + +- [ ] **Step 4: Verify imports resolve (no `next-themes` references remain)** + +```bash +grep -r "next-themes" frontend/src/components/ui/ frontend/src/hooks/ +``` + +Expected: Only `frontend/src/components/ui/sonner.tsx` should match (to be fixed in next step). + +- [ ] **Step 5: Fix sonner.tsx — rewrite next-themes import and theme derivation** + +In `frontend/src/components/ui/sonner.tsx`, replace the import and theme logic: + +```diff +- import { useTheme } from "next-themes" ++ import { useTheme } from "@/components/theme-provider" ++ import { THEMES } from "@/atoms/theme" +``` + +Replace the theme destructuring inside the `Toaster` component: + +```diff +- const { theme = "system" } = useTheme() ++ const { resolvedTheme } = useTheme() ++ const themeConfig = THEMES.find((t) => t.name === resolvedTheme) ++ const theme = themeConfig?.colorScheme === "dark" ? "dark" : "light" +``` + +- [ ] **Step 6: Verify no remaining `next-themes` references** + +```bash +grep -r "next-themes" frontend/src/ +``` + +Expected: No matches. + +- [ ] **Step 7: Commit** + +```bash +git add frontend/src/components/ui/ frontend/src/hooks/use-mobile.ts +git commit -m "feat: drop-replace UI components with radix-nova style set" +``` + +--- + +## Task 3: Reset CSS theme + +**Files:** +- Rewrite: `frontend/src/index.css` +- Delete: `frontend/src/plugins/tailwind-mix.js` + +- [ ] **Step 1: Rewrite `index.css`** + +Replace the entire file with the new theme. Key changes: +- Use zinc light/dark palette from the zip's `app/globals.css` (at `/tmp/newui/app/globals.css`) +- Add `--destructive-foreground` to both `:root` and `.dark` +- Keep `@import "shadcn/tailwind.css"` and `@import "tw-animate-css"` +- Remove `@fontsource-variable/*` imports +- Remove `@plugin './plugins/tailwind-mix.js'` and `@plugin '@tailwindcss/typography'` +- Remove all `--font-heading` / `--font-sans` custom overrides +- Remove the `h1-h6` rule block that uses `font-family: var(--font-heading)` +- Keep `.theme-*` class scaffolding as empty comments for future use +- Keep `--breakpoint-ssm` and `--breakpoint-xs` if used + +The new `index.css` should be structured as: +```css +@import 'tailwindcss'; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; +@custom-variant dark (&:is(.dark *)); + +@theme inline { + /* Use the zip's @theme inline block from /tmp/newui/app/globals.css */ + /* Keep --breakpoint-ssm: 320px and --breakpoint-xs: 475px from current */ + /* Include: --color-destructive-foreground: var(--destructive-foreground); */ +} + +:root { + /* Zinc light palette from /tmp/newui/app/globals.css */ + /* Add: --destructive-foreground: oklch(0.985 0 0); (white — for text on destructive backgrounds) */ +} + +.dark { + /* Zinc dark palette from /tmp/newui/app/globals.css */ + /* Add: --destructive-foreground: oklch(0.985 0 0); */ +} + +/* Future themes — empty scaffolding */ +/* .theme-aurora-classic { } */ +/* .theme-morning { } */ +/* .theme-sunset.dark { } */ +/* .theme-midnight.dark { } */ + +@layer base { + * { @apply border-border outline-ring/50; } + body { @apply bg-background text-foreground; } +} +``` + +- [ ] **Step 2: Delete tailwind-mix plugin and empty directory** + +```bash +rm frontend/src/plugins/tailwind-mix.js +rmdir frontend/src/plugins/ 2>/dev/null || true +``` + +- [ ] **Step 3: Global find-and-replace `font-heading` → `font-sans`** + +```bash +cd frontend && grep -rl "font-heading" src/ --include="*.tsx" --include="*.ts" | xargs sed -i 's/font-heading/font-sans/g' +``` + +Note: This is safe because `font-heading` only appears as a Tailwind class name, never as a variable name or string literal (other than in CSS which we just rewrote). + +- [ ] **Step 4: Verify no `font-heading` references remain** + +```bash +grep -r "font-heading" frontend/src/ +``` + +Expected: No matches. + +- [ ] **Step 5: Commit** + +```bash +git add frontend/src/index.css frontend/src/plugins/ +git add -u frontend/src/ # catch the font-heading replacements +git commit -m "style: reset CSS to zinc light/dark theme, remove custom fonts" +``` + +--- + +## Task 4: Migrate notification system to Sonner + +**Files:** +- Modify: `frontend/src/graphql.ts:16-33` +- Modify: `frontend/src/features/server/ServerRow.tsx:9` (import), usage lines +- Modify: `frontend/src/features/server/ServerInfoModal.tsx:10` (import), usage lines +- Modify: `frontend/src/features/auth/EmailPasswordForm.tsx:15` (import), usage lines +- Modify: `frontend/src/main.tsx:11` (add Toaster import) +- Modify: `frontend/src/App.tsx:14,46` (remove Notification lazy import and render) +- Delete: `frontend/src/features/Notification.tsx` +- Delete: `frontend/src/atoms/notification.ts` +- Delete: `frontend/src/atoms/notificationManager.ts` + +- [ ] **Step 1: Add Sonner `` to `main.tsx`** + +In `frontend/src/main.tsx`, add the import and render the Toaster inside the provider stack, after ``: + +```diff ++ import { Toaster } from "@/components/ui/sonner" +``` + +Add `` as a sibling of `` inside ``: + +```diff + }> + ++ + +``` + +- [ ] **Step 2: Migrate `graphql.ts` — replace `notify()` with `toast()`** + +In `frontend/src/graphql.ts`: + +```diff +- import { notify } from "./atoms/notification"; ++ import { toast } from "sonner"; +``` + +Replace the error handler body (lines 20-33): + +```diff + if (graphQLErrors) + graphQLErrors.forEach(({ message }) => +- notify({ +- title: i18n.t("GraphQL error"), +- body: message, +- type: "error", +- }) ++ toast.error(i18n.t("GraphQL error"), { description: message }) + ); + if (networkError) +- notify({ +- title: i18n.t("Network error"), +- body: networkError.message, +- type: "error", +- }) ++ toast.error(i18n.t("Network error"), { description: networkError.message }) +``` + +- [ ] **Step 3: Migrate `ServerInfoModal.tsx` — replace `notify()` with `toast()`** + +In `frontend/src/features/server/ServerInfoModal.tsx`: + +```diff +- import { notify } from "../../atoms/notification"; ++ import { toast } from "sonner"; +``` + +Find all `notify({...})` calls in this file and replace with equivalent `toast()` / `toast.error()` / `toast.success()` calls. + +> **Note:** This file will still have compile errors after this step because it also imports `useModal` and `ModalShell` (deleted in Task 5). Those imports are fixed in Task 8 (Wave 2) when the file is converted to inline Dialog. This is expected during the transition period. + +- [ ] **Step 4: Migrate `ServerRow.tsx` — replace `useNotificationsReducer` with `toast()`** + +In `frontend/src/features/server/ServerRow.tsx`: + +```diff +- import { useNotificationsReducer } from "../../atoms/notification"; ++ import { toast } from "sonner"; +``` + +Remove the `useNotificationsReducer()` hook call and replace all `addNotification({...})` calls with `toast()` / `toast.error()` / `toast.success()` calls. + +- [ ] **Step 5: Migrate `EmailPasswordForm.tsx` — replace `useNotificationsReducer` with `toast()`** + +In `frontend/src/features/auth/EmailPasswordForm.tsx`: + +```diff +- import { useNotificationsReducer } from "../../atoms/notification"; ++ import { toast } from "sonner"; +``` + +Remove the `useNotificationsReducer()` hook call and replace all `addNotification({...})` calls with `toast()` / `toast.error()` / `toast.success()` calls. + +- [ ] **Step 6: Remove `` from `App.tsx`** + +In `frontend/src/App.tsx`: + +```diff +- const Notification = lazy(() => import("./features/Notification")); +``` + +```diff +- +``` + +- [ ] **Step 7: Delete old notification files** + +```bash +rm frontend/src/features/Notification.tsx +rm frontend/src/atoms/notification.ts +rm frontend/src/atoms/notificationManager.ts +``` + +- [ ] **Step 8: Verify no remaining notification atom imports** + +```bash +grep -r "atoms/notification" frontend/src/ +``` + +Expected: No matches. + +- [ ] **Step 9: Commit** + +```bash +git add -A frontend/src/ +git commit -m "feat: migrate notification system from Jotai atoms to Sonner toasts" +``` + +--- + +## Task 5: Remove modal manager system + +**Files:** +- Modify: `frontend/src/App.tsx:13,44` (remove ModalManager lazy import and render) +- Delete: `frontend/src/features/modal/ModalManager.tsx` +- Delete: `frontend/src/features/modal/ConfirmationModal.tsx` +- Delete: `frontend/src/features/ui/ModalShell.tsx` +- Delete: `frontend/src/atoms/modal.ts` + +Note: This task only removes the centralized modal infrastructure. Individual modal components (ServerInfoModal, PortFunctionModal, etc.) will be converted to inline Dialog usage in their respective wave tasks. Until then they will be broken — this is acceptable per the spec's "Known Transition Period." + +- [ ] **Step 1: Remove `` from `App.tsx`** + +In `frontend/src/App.tsx`: + +```diff +- const ModalManager = lazy(() => import("./features/modal/ModalManager")); +``` + +```diff +- +``` + +- [ ] **Step 2: Delete modal infrastructure files and empty directories** + +```bash +rm frontend/src/features/modal/ModalManager.tsx +rm frontend/src/features/modal/ConfirmationModal.tsx +rmdir frontend/src/features/modal/ +rm frontend/src/features/ui/ModalShell.tsx +rm frontend/src/atoms/modal.ts +``` + +- [ ] **Step 3: Delete layout atom** + +```bash +rm frontend/src/atoms/layout.ts +``` + +This file only contains `drawerOpenAtom`, which will be replaced by the sidebar's internal state. + +- [ ] **Step 4: Commit** + +```bash +git add -A frontend/src/ +git commit -m "refactor: remove centralized Jotai modal manager and layout atom" +``` + +--- + +## Task 6: Rebuild Layout shell with shadcn Sidebar + sticky navbar + +**Files:** +- Rewrite: `frontend/src/Layout.tsx` +- Delete: `frontend/src/features/layout/SideBar.tsx` +- Delete: `frontend/src/features/layout/NavBar.tsx` +- Modify: `frontend/src/components/ui/sidebar.tsx` (cookie → localStorage persistence) + +- [ ] **Step 1: Fix sidebar.tsx cookie persistence → localStorage** + +In `frontend/src/components/ui/sidebar.tsx`, find the cookie-based persistence logic. Replace `document.cookie` reads/writes with `localStorage.getItem("sidebar:state")` / `localStorage.setItem("sidebar:state", ...)`. The sidebar component uses a `SIDEBAR_COOKIE_NAME` constant and `SIDEBAR_COOKIE_MAX_AGE` — replace these with a `SIDEBAR_STORAGE_KEY` constant and use localStorage instead. + +Key changes: +- Replace `document.cookie = ...` with `localStorage.setItem(SIDEBAR_STORAGE_KEY, ...)` +- Replace cookie parsing for initial state with `localStorage.getItem(SIDEBAR_STORAGE_KEY)` +- Remove `SIDEBAR_COOKIE_MAX_AGE` + +- [ ] **Step 2: Rewrite `Layout.tsx`** + +Replace the entire file. The new layout uses shadcn Sidebar + sticky navbar pattern: + +```tsx +import { Suspense } from "react"; +import { Outlet, useNavigate, NavLink } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { + Check, + CircleUserRound, + Languages, + LogOut, + Palette, +} from "lucide-react"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarHeader, + SidebarInset, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar"; +import { Separator } from "@/components/ui/separator"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import ThemedSuspense from "@/features/ThemedSuspense"; +import { useAuthReducer } from "@/atoms/auth"; +import { useTheme } from "@/components/theme-provider"; +import { THEMES } from "@/atoms/theme"; +import { routes, type RouteConfig } from "@/routes"; +import { cn } from "@/lib/utils"; + +/* --- Implementation notes for the agent --- + * + * 1. Filter routes with `nav: true` from routes.ts for sidebar items. + * 2. Use NavLink with SidebarMenuButton for active-state styling. + * 3. The sticky navbar header goes inside SidebarInset, above Outlet. + * 4. Port the account dropdown (theme/language/logout) from the old NavBar.tsx + * into the sticky header's right side. + * 5. Port the brand area ("Aurora" + dot) into SidebarHeader. + * 6. Preserve the auth check + websocket init from the current Layout.tsx + * (useEffect with token, initializeWebSocket, getToken). + */ +``` + +The structure should match the spec's code skeleton: +```tsx + + + {/* Brand: dot + "Aurora" */} + + + + + {sidebarRoutes.map((route) => ( + + + + + {t(route.labelKey)} + + + + ))} + + + + + {/* optional: theme/lang if desired here */} + + +
+ + +
+ {/* Account dropdown with theme picker, language, logout */} +
+
+ }> +
+
+ +
+
+
+
+
+
+``` + +Preserve the auth/websocket `useEffect` from the current `Layout.tsx` (lines 22-41). + +- [ ] **Step 3: Delete old layout files** + +```bash +rm frontend/src/features/layout/SideBar.tsx +rm frontend/src/features/layout/NavBar.tsx +``` + +- [ ] **Step 4: Verify build compiles** + +```bash +cd frontend && npx tsc --noEmit 2>&1 | head -50 +``` + +Expected: May show errors in page components that still reference deleted modal/notification imports — these will be fixed in wave tasks. The layout shell itself should compile. + +- [ ] **Step 5: Commit** + +```bash +git add -A frontend/src/ +git commit -m "feat: rebuild layout shell with shadcn Sidebar + sticky navbar" +``` + +--- + +## Task 7: Wave 1 — Auth pages + +**Files:** +- Modify: `frontend/src/features/auth/Login.tsx` +- Modify: `frontend/src/features/auth/CreateAccount.tsx` +- Modify: `frontend/src/features/auth/EmailPasswordForm.tsx` +- Modify: `frontend/src/features/theme/ThemeSwitch.tsx` +- Modify: `frontend/src/features/theme/ThemeMenuItems.tsx` +- Modify: `frontend/src/features/i18n/LanguageSwitch.tsx` +- Modify: `frontend/src/features/i18n/LanguageMenuItems.tsx` + +- [ ] **Step 1: Update `EmailPasswordForm.tsx`** + +This is the shared form component. Update: +- Component imports to use new radix-nova `Input`, `Button`, `Label` (import paths stay the same `@/components/ui/*`, but verify variant/size prop compatibility) +- The `addNotification` calls were already migrated to `toast()` in Task 4 +- Check for any `font-heading` classes (already replaced globally in Task 3) +- Remove any remaining `useModal` imports if present + +- [ ] **Step 2: Update `Login.tsx`** + +Update component imports and variant props. This page is a simple form wrapper — minimal changes expected beyond ensuring new button/input sizing looks correct. + +- [ ] **Step 3: Update `CreateAccount.tsx`** + +Same treatment as Login.tsx. + +- [ ] **Step 4: Update `ThemeSwitch.tsx` and `LanguageSwitch.tsx`** + +These components are used in the Layout sidebar footer and auth pages. Update variant/size props to match radix-nova components (`Button`, `DropdownMenu`). `ThemeSwitch.tsx` can be simplified to a light/dark toggle since we're on zinc only for now, though the full theme infrastructure still works. + +Also review `ThemeMenuItems.tsx` and `LanguageMenuItems.tsx` for the same prop compatibility. + +- [ ] **Step 5: Smoke test the auth flow** + +```bash +cd frontend && npx tsc --noEmit 2>&1 | grep -E "Login|CreateAccount|EmailPasswordForm|ThemeSwitch|LanguageSwitch" +``` + +Expected: No type errors in these files. + +- [ ] **Step 6: Commit** + +```bash +git add frontend/src/features/auth/ frontend/src/features/theme/ frontend/src/features/i18n/ +git commit -m "feat(wave-1): migrate auth, theme, and language pages to radix-nova components" +``` + +--- + +## Task 8: Wave 2 — Server pages + +**Files:** +- Modify: `frontend/src/features/server/ServerList.tsx` +- Modify: `frontend/src/features/server/ServerRow.tsx` +- Modify: `frontend/src/features/server/ServerCard.tsx` +- Modify: `frontend/src/features/server/ServerContainer.tsx` +- Modify: `frontend/src/features/server/ServerStat.tsx` +- Modify: `frontend/src/features/server/ServerPortsStat.tsx` +- Modify: `frontend/src/features/server/ServerTrafficStat.tsx` +- Modify: `frontend/src/features/server/ServerSSHStat.tsx` +- Modify: `frontend/src/features/server/ServerInfoModal.tsx` +- Modify: `frontend/src/features/server/chart/Chart.tsx` +- Modify: `frontend/src/features/server/chart/Sparkline.tsx` +- Modify: `frontend/src/hooks/useServerItem.ts` + +- [ ] **Step 1: Convert `ServerInfoModal.tsx` to inline Dialog** + +This is the most complex change in this wave. The file currently uses `useModal()` (line 7) and `ModalShell` (line 11). Convert to: +- Remove `useModal` import, add `useState` +- Remove `ModalShell` import, add `Dialog`, `DialogContent`, `DialogHeader`, `DialogTitle`, `DialogFooter` imports +- Replace `ModalShell` wrapper with `DialogContent` + `DialogHeader` + `DialogFooter` +- The modal is currently opened via `useModal().open("serverInfo", {...})` from `ServerList.tsx` — change to pass `open`/`onOpenChange` props or move the Dialog inline into the parent +- The `notify()` calls were already migrated to `toast()` in Task 4 + +- [ ] **Step 2: Update `ServerList.tsx`** + +- Remove `useModal` import (line 30) +- Add `useState` for controlling the ServerInfoModal dialog +- Render `` inline with Dialog wrapper +- Update `Card`, `Badge`, `Button` variant props if needed + +- [ ] **Step 3: Update `useServerItem.ts`** + +- Remove `useModal` import (line 2) +- This hook uses `useModal` to open server info — refactor to return a callback or state setter instead + +- [ ] **Step 4: Update `ServerRow.tsx` and `ServerCard.tsx`** + +- Update `Card`, `Badge`, `Button`, `DropdownMenu` variant/size props +- `ServerRow.tsx` notification migration was done in Task 4 +- Remove any remaining `useModal` references + +- [ ] **Step 5: Update stat components** + +Update `ServerStat.tsx`, `ServerPortsStat.tsx`, `ServerTrafficStat.tsx`, `ServerSSHStat.tsx`: +- Update `Card` imports and variant props +- These are mainly presentational — minimal changes + +- [ ] **Step 6: Update `ServerContainer.tsx`** + +Update any layout-related imports. This is the parent route component. + +- [ ] **Step 7: Review chart components** + +`chart/Chart.tsx` and `chart/Sparkline.tsx` use Recharts. Check if they should wrap in the new `chart.tsx` component from the zip. If they work as-is, leave them. + +- [ ] **Step 8: Type-check server pages** + +```bash +cd frontend && npx tsc --noEmit 2>&1 | grep -E "features/server/" +``` + +Expected: No type errors. + +- [ ] **Step 9: Commit** + +```bash +git add frontend/src/features/server/ frontend/src/hooks/useServerItem.ts +git commit -m "feat(wave-2): migrate server pages to radix-nova + inline dialogs" +``` + +--- + +## Task 9: Wave 3 — Port & User pages + +**Files:** +- Modify: `frontend/src/features/port/ServerPorts.tsx` +- Modify: `frontend/src/features/port/PortCard.tsx` +- Modify: `frontend/src/features/port/PortSelectCard.tsx` +- Modify: `frontend/src/features/port/PortUsersCard.tsx` +- Modify: `frontend/src/features/port/PortFunctionModal.tsx` +- Modify: `frontend/src/features/port/PortRestrictionModal.tsx` +- Modify: `frontend/src/features/port/restriction/PortExpiration.tsx` +- Modify: `frontend/src/features/user/Users.tsx` +- Modify: `frontend/src/features/user/ServerUsers.tsx` + +- [ ] **Step 1: Convert `PortFunctionModal.tsx` to inline Dialog** + +Remove `useModal` + `ModalShell` pattern. Replace with `Dialog` + `DialogContent` + local state. The parent (`PortCard.tsx` or `ServerPorts.tsx`) should render the Dialog inline. + +- [ ] **Step 2: Convert `PortRestrictionModal.tsx` to inline Dialog** + +Same pattern as PortFunctionModal. + +- [ ] **Step 3: Update `ServerPorts.tsx`** + +- Remove `useModal` import (line 13) +- Add inline Dialog rendering for port modals +- Update `Card`, `Select`, `Badge` props + +- [ ] **Step 4: Update `PortCard.tsx` and `PortSelectCard.tsx`** + +- Remove `useModal` imports +- Update component variant props + +- [ ] **Step 5: Update `PortUsersCard.tsx` and `PortExpiration.tsx`** + +Update imports and props. + +- [ ] **Step 6: Update `Users.tsx` and `ServerUsers.tsx`** + +Update `Table`, `Button`, `Badge` imports and props. + +- [ ] **Step 7: Type-check** + +```bash +cd frontend && npx tsc --noEmit 2>&1 | grep -E "features/(port|user)/" +``` + +- [ ] **Step 8: Commit** + +```bash +git add frontend/src/features/port/ frontend/src/features/user/ +git commit -m "feat(wave-3): migrate port and user pages to radix-nova + inline dialogs" +``` + +--- + +## Task 10: Wave 4 — Files & Deployments + +**Files:** +- Modify: `frontend/src/features/file/FileCenter.tsx` +- Modify: `frontend/src/features/file/FileCenterContainer.tsx` +- Modify: `frontend/src/features/file/FileCard.tsx` +- Modify: `frontend/src/features/file/FileRow.tsx` +- Modify: `frontend/src/features/file/FileModal.tsx` +- Modify: `frontend/src/features/file/FilePreviewModal.tsx` +- Modify: `frontend/src/features/deployment/DeploymentList.tsx` +- Modify: `frontend/src/features/deployment/DeploymentStatusBadge.tsx` +- Modify: `frontend/src/features/deployment/DeployModal.tsx` +- Modify: `frontend/src/features/deployment/DeploymentDetailModal.tsx` +- Modify: `frontend/src/features/deployment/BindingModal.tsx` + +- [ ] **Step 1: Convert file modals to inline Dialog** + +`FileModal.tsx` and `FilePreviewModal.tsx` — remove `ModalShell`, replace with `Dialog`/`DialogContent`. `FilePreviewModal` may suit `Sheet` for a slide-over preview. + +- [ ] **Step 2: Update `FileCenter.tsx`, `FileCard.tsx`, `FileRow.tsx`** + +- Remove `useModal` imports +- Render file dialogs inline +- Update `Card`, `Table`, `Badge` props + +- [ ] **Step 3: Convert deployment modals to inline Dialog** + +`DeployModal.tsx`, `DeploymentDetailModal.tsx`, `BindingModal.tsx` — same pattern: remove `ModalShell` + `useModal`, replace with inline `Dialog`. + +- [ ] **Step 4: Update `DeploymentList.tsx` and `DeploymentStatusBadge.tsx`** + +- Remove `useModal` imports +- Render deployment dialogs inline +- Update `Badge`, `Table` props + +- [ ] **Step 5: Update `FileCenterContainer.tsx`** + +Minor layout updates. + +- [ ] **Step 6: Type-check** + +```bash +cd frontend && npx tsc --noEmit 2>&1 | grep -E "features/(file|deployment)/" +``` + +- [ ] **Step 7: Commit** + +```bash +git add frontend/src/features/file/ frontend/src/features/deployment/ +git commit -m "feat(wave-4): migrate file and deployment pages to radix-nova + inline dialogs" +``` + +--- + +## Task 11: Wave 5 — Services & Misc pages + +**Files:** +- Modify: `frontend/src/features/service-editor/ServiceListPage.tsx` +- Modify: `frontend/src/features/service-editor/ServiceEditorPage.tsx` +- Modify: `frontend/src/features/service-editor/AuthoringJsonPanel.tsx` +- Modify: `frontend/src/features/service-editor/FormPreviewPanel.tsx` +- Modify: `frontend/src/features/service-editor/CompileOutputPanel.tsx` +- Modify: `frontend/src/features/service-editor/ParamEditorPanel.tsx` +- Modify: `frontend/src/features/service-editor/ParamList.tsx` +- Modify: `frontend/src/features/service-editor/ParamTypeEditor.tsx` +- Modify: `frontend/src/features/service-editor/EmitConfigEditor.tsx` +- Modify: `frontend/src/features/service-editor/ValidationEditor.tsx` +- Modify: `frontend/src/features/service-editor/ConditionEditor.tsx` +- Modify: `frontend/src/features/service-editor/UIConfigEditor.tsx` +- Modify: `frontend/src/features/service-editor/fields/TextField.tsx` +- Modify: `frontend/src/features/service-editor/fields/SelectField.tsx` +- Modify: `frontend/src/features/service-editor/fields/CheckboxField.tsx` +- Modify: `frontend/src/features/service-editor/fields/TextAreaField.tsx` +- Modify: `frontend/src/features/service-editor/fields/ListField.tsx` +- Modify: `frontend/src/features/service-editor/fields/ObjectField.tsx` +- Modify: `frontend/src/features/service-editor/fields/FieldsRenderer.tsx` +- Modify: `frontend/src/features/service-editor/fields/FieldShell.tsx` +- Modify: `frontend/src/features/service-editor/fields/FieldError.tsx` +- Modify: `frontend/src/features/about/About.tsx` +- Modify: `frontend/src/features/layout/Themes.tsx` +- Modify: `frontend/src/features/layout/Error.tsx` +- Modify: `frontend/src/features/layout/NoMatch.tsx` +- Modify: `frontend/src/features/layout/Hero.tsx` + +- [ ] **Step 1: Update `ServiceListPage.tsx`** + +- Remove `useModal` import (line 17) +- Update `Card`, `Button`, `Table` variant props + +- [ ] **Step 2: Update `ServiceEditorPage.tsx` and panels** + +Update `Tabs`, `Card`, `Button`, `Input` props across `ServiceEditorPage.tsx`, `AuthoringJsonPanel.tsx`, `FormPreviewPanel.tsx`, `CompileOutputPanel.tsx`, `ParamEditorPanel.tsx`. Consider using `Resizable` from the new components for the editor split-pane if applicable. + +- [ ] **Step 3: Update service editor field components** + +Update all files in `fields/` — these use `Input`, `Select`, `Checkbox`, `Textarea`, `Label`. Import paths stay the same; verify variant/size props are compatible. + +- [ ] **Step 4: Update remaining service editor files** + +`ParamList.tsx`, `ParamTypeEditor.tsx`, `EmitConfigEditor.tsx`, `ValidationEditor.tsx`, `ConditionEditor.tsx`, `UIConfigEditor.tsx` — update `Input`, `Select`, `Button` imports and props. + +- [ ] **Step 5: Update `Themes.tsx`** + +Simplify since we're on zinc light/dark only. The theme picker page can still show the available themes from the `THEMES` array, but only light/dark will be visually distinct. + +- [ ] **Step 6: Update misc pages** + +`About.tsx`, `Error.tsx`, `NoMatch.tsx`, `Hero.tsx` — minimal updates. + +- [ ] **Step 7: Type-check** + +```bash +cd frontend && npx tsc --noEmit 2>&1 | grep -E "features/(service-editor|about|layout)/" +``` + +- [ ] **Step 8: Commit** + +```bash +git add frontend/src/features/service-editor/ frontend/src/features/about/ frontend/src/features/layout/ +git commit -m "feat(wave-5): migrate service editor, about, and misc pages to radix-nova" +``` + +--- + +## Task 12: Update shared utilities + +**Files:** +- Modify: `frontend/src/features/ui/PageHeader.tsx` +- Modify: `frontend/src/features/ui/PageSection.tsx` +- Modify: `frontend/src/features/ui/EmptyState.tsx` +- Modify: `frontend/src/features/DataLoading.tsx` +- Modify: `frontend/src/features/Paginator.tsx` +- Modify: `frontend/src/features/ThemedSuspense.tsx` + +- [ ] **Step 1: Update `PageHeader.tsx`** + +Update to use new component primitives. The `font-heading` class was already replaced globally in Task 3. + +- [ ] **Step 2: Update `PageSection.tsx` and `EmptyState.tsx`** + +Update component imports and props. + +- [ ] **Step 3: Update `DataLoading.tsx`** + +May use new `Spinner` or `Skeleton` from radix-nova set. + +- [ ] **Step 4: Update `Paginator.tsx`** + +Update `Button` variant props. + +- [ ] **Step 5: Update `ThemedSuspense.tsx`** + +Simplify loading state with new `Spinner` or `Skeleton`. + +- [ ] **Step 6: Commit** + +```bash +git add frontend/src/features/ui/ frontend/src/features/DataLoading.tsx frontend/src/features/Paginator.tsx frontend/src/features/ThemedSuspense.tsx +git commit -m "feat: update shared UI utilities to radix-nova primitives" +``` + +--- + +## Task 13: Final cleanup and dependency removal + +**Files:** +- Modify: `frontend/package.json` +- Verify: entire `frontend/src/` + +- [ ] **Step 1: Remove unused font packages** + +```bash +cd frontend && npm uninstall @fontsource-variable/noto-sans @fontsource-variable/space-grotesk +``` + +- [ ] **Step 2: Full type-check** + +```bash +cd frontend && npx tsc --noEmit +``` + +Expected: No errors. + +- [ ] **Step 3: Verify no dead imports** + +```bash +grep -r "atoms/modal" frontend/src/ +grep -r "atoms/notification" frontend/src/ +grep -r "atoms/layout" frontend/src/ +grep -r "ModalShell" frontend/src/ +grep -r "useModal" frontend/src/ +grep -r "ModalManager" frontend/src/ +grep -r "drawerOpenAtom" frontend/src/ +grep -r "font-heading" frontend/src/ +grep -r "next-themes" frontend/src/ +grep -r "notificationManager" frontend/src/ +``` + +Expected: No matches for any of these. + +- [ ] **Step 4: Verify the `modal/` directory is gone** + +```bash +ls frontend/src/features/modal/ 2>&1 +``` + +Expected: "No such file or directory" + +- [ ] **Step 5: Build check** + +```bash +cd frontend && npm run build +``` + +Expected: Build succeeds. + +- [ ] **Step 6: Commit** + +```bash +git add -A frontend/ +git commit -m "chore: final cleanup — remove unused fonts and verify no dead imports" +``` diff --git a/docs/superpowers/plans/2026-03-24-shadcn-migration.md b/docs/superpowers/plans/2026-03-24-shadcn-migration.md new file mode 100644 index 0000000..0bc8746 --- /dev/null +++ b/docs/superpowers/plans/2026-03-24-shadcn-migration.md @@ -0,0 +1,1547 @@ +# Frontend Migration: DaisyUI to shadcn/ui + TypeScript + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate the Aurora frontend from DaisyUI 5 + JavaScript to shadcn/ui + TypeScript, upgrade React 18→19, remove Redux in favor of Jotai, and refactor the service editor. + +**Architecture:** Build shadcn component library on top of Radix UI primitives with CVA variants. Multi-theme system using CSS classes on `` that set shadcn CSS variables. Incremental in-place feature migration while DaisyUI coexists — DaisyUI degradation is acceptable during the migration window. + +**Tech Stack:** React 19, TypeScript (strict), Vite 7 + SWC, Tailwind CSS 4, shadcn/ui (Radix + CVA), Jotai, Apollo Client, Framer Motion, i18next + +**Spec:** `docs/superpowers/specs/2026-03-24-shadcn-migration-design.md` + +**Known codebase quirks:** +- `frontend/src/features/auth/CreateAccoount.jsx` has a typo (double "o") — rename to `CreateAccount.tsx` during migration +- `frontend/src/polyfills/symbolObservable.js` is imported in `main.jsx` — was needed for old Redux; remove when Redux is removed +- `frontend/src/plugins/tailwind-mix.js` is imported in `index.css` — keep as-is per spec (available but not preferred for new code) +- `react-loading` is used in `DataLoading.jsx`, `ThemedSuspense.jsx`, `ServerSSHStat.jsx` — uses `createReactClass`, likely incompatible with React 19; replace with shadcn `Skeleton` or CSS spinner during React 19 upgrade +- `phosphor-react` is in `package.json` but unused — remove in Phase 5 +- `frontend/codegen.yml` exists and is configured for RTK Query (`typescript-rtk-query` plugin) — must be updated in Task 9 +- `postcss.config.cjs` and `tailwind.config.cjs` are vestigial with Tailwind v4 CSS config — review in Phase 5 + +**Scope note for feature tasks (Phases 2-4):** When a task says "All files in `frontend/src/features/X/`", this means **all files including subdirectories**. The Files section lists key files but is not exhaustive — convert every `.jsx`/`.js` file in the directory. + +--- + +## Foundation + +### Task 1: TypeScript + Package Foundation + +**Files:** +- Modify: `frontend/package.json` +- Replace: `frontend/tsconfig.json` (existing file has incompatible structure — overwrite entirely) +- Create: `frontend/tsconfig.app.json` +- Create: `frontend/tsconfig.node.json` +- Create: `frontend/src/vite-env.d.ts` +- Modify: `frontend/vite.config.js` → `frontend/vite.config.ts` +- Delete: `frontend/jsconfig.json` + +- [ ] **Step 1: Install new dependencies** + +```bash +cd frontend +npm install @fontsource-variable/noto-sans class-variance-authority clsx tailwind-merge radix-ui shadcn tw-animate-css react-helmet-async +npm install -D typescript @types/react @types/react-dom @types/node +``` + +- [ ] **Step 2: Upgrade React to 19** + +```bash +cd frontend +npm install react@^19 react-dom@^19 +npm install -D @types/react@^19 @types/react-dom@^19 +``` + +Check for peer dependency conflicts. If `react-helmet` fails on peer deps, use `--legacy-peer-deps`. It will be removed in Task 6. + +**Important:** `react-loading` uses `createReactClass` which is incompatible with React 19. Before proceeding, replace it: + +1. Uninstall: `npm uninstall react-loading` +2. Find consumers: `grep -r "react-loading" src/ -l` (expect: `DataLoading.jsx`, `ThemedSuspense.jsx`, `ServerSSHStat.jsx`) +3. Replace each with a simple CSS spinner or shadcn Skeleton. For `ThemedSuspense.jsx` (used as Suspense fallback in `main.jsx`): + +```tsx +export default function ThemedSuspense() { + return ( +
+
+
+ ); +} +``` + +Apply same pattern for `DataLoading.jsx`. `ServerSSHStat.jsx` can use shadcn `Skeleton` once available (Task 4), or the CSS spinner for now. + +- [ ] **Step 3: Create tsconfig files** + +`frontend/tsconfig.json` — project references root: +```json +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} +``` + +`frontend/tsconfig.app.json`: +```json +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": false, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "allowJs": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} +``` + +Note: `allowJs: true` enables incremental migration. `noUnusedLocals` and `noUnusedParameters` set to `false` during migration to avoid noise from partially-converted files. Tighten after Phase 5. + +`frontend/tsconfig.node.json`: +```json +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": false, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} +``` + +- [ ] **Step 4: Create vite-env.d.ts** + +`frontend/src/vite-env.d.ts`: +```typescript +/// +``` + +- [ ] **Step 5: Rename vite.config.js → vite.config.ts** + +Convert `frontend/vite.config.js` to TypeScript. Keep `@vitejs/plugin-react-swc` (not the Babel plugin from shadcn-template). Keep the dev proxy config. + +```typescript +import path from "path" +import tailwindcss from "@tailwindcss/vite" +import react from "@vitejs/plugin-react-swc" +import svgr from "vite-plugin-svgr" +import { defineConfig } from "vite" + +export default defineConfig({ + plugins: [tailwindcss(), svgr(), react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + server: { + proxy: { + "/api": { + target: "http://localhost:8000", + changeOrigin: true, + }, + }, + }, +}) +``` + +- [ ] **Step 6: Delete jsconfig.json** + +Remove `frontend/jsconfig.json` — superseded by tsconfig.json. + +- [ ] **Step 7: Add components.json for shadcn CLI** + +`frontend/components.json`: +```json +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "radix-maia", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "olive", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} +``` + +- [ ] **Step 8: Verify the app still builds** + +```bash +cd frontend && npm run build +``` + +Fix any TypeScript or dependency errors. The app should still work with existing JSX files since `allowJs: true`. + +- [ ] **Step 9: Commit** + +```bash +git add frontend/ +git commit -m "feat: add TypeScript config and shadcn dependencies" +``` + +--- + +### Task 2: CSS Foundation + Theme Variables + +**Files:** +- Modify: `frontend/src/index.css` +- Create: `frontend/src/lib/utils.ts` +- Create: `frontend/src/styles/daisyui-themes-reference.css` + +- [ ] **Step 1: Create cn() utility** + +`frontend/src/lib/utils.ts`: +```typescript +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} +``` + +- [ ] **Step 2: Copy existing DaisyUI theme definitions to reference file** + +Copy the 3 custom `@plugin 'daisyui/theme'` blocks from `frontend/src/index.css` into `frontend/src/styles/daisyui-themes-reference.css` (not imported, reference only). + +- [ ] **Step 3: Fix --border variable conflict** + +In the DaisyUI theme blocks in `frontend/src/index.css`, rename `--border: 1px` to `--border-width: 1px` in all three custom themes (aurora-classic, sunset, morning). This is a **required** step — DaisyUI uses `--border` for border width, but shadcn uses it for border color. They will conflict. + +- [ ] **Step 4: Add shadcn CSS layer to index.css** + +Add these imports after the existing DaisyUI imports in `frontend/src/index.css`: + +```css +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; +@import "@fontsource-variable/noto-sans"; +``` + +Add the `@custom-variant` directive: +```css +@custom-variant dark (&:is(.dark *)); +``` + +Add the `@theme inline` block with shadcn variable mappings (copy from `shadcn-template/src/index.css` lines 8-49). + +Add custom breakpoints inside the `@theme inline` block: +```css + --breakpoint-ssm: 320px; + --breakpoint-xs: 475px; +``` + +- [ ] **Step 5: Add Aurora theme definitions** + +Add 4 theme classes after the `@theme inline` block. Each sets all shadcn CSS variables. + +`:root` block = aurora-classic (light, default fallback). + +`.theme-aurora-classic` = same values as `:root`. + +`.theme-sunset.dark` = dark theme with coral primary. Derive OKLCH values from existing DaisyUI sunset theme colors (#151726 base, #EE8679 primary). + +`.theme-morning` = light theme with terracotta primary. Derive from existing DaisyUI morning theme (#FDFDFE base, #D26A5D primary). + +`.theme-midnight.dark` = new dark theme with purple primary. Design purple-toned dark palette. + +Each theme must define: `--background`, `--foreground`, `--card`, `--card-foreground`, `--popover`, `--popover-foreground`, `--primary`, `--primary-foreground`, `--secondary`, `--secondary-foreground`, `--muted`, `--muted-foreground`, `--accent`, `--accent-foreground`, `--destructive`, `--border`, `--input`, `--ring`, `--chart-1` through `--chart-5`, `--radius`, `--sidebar` and all `--sidebar-*` variants. + +- [ ] **Step 6: Add base layer styles** + +```css +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + html { + font-family: var(--font-sans); + } +} +``` + +- [ ] **Step 7: Verify build still works** + +```bash +cd frontend && npm run build +``` + +DaisyUI and shadcn CSS should coexist. DaisyUI styling degradation is acceptable. + +- [ ] **Step 8: Commit** + +```bash +git add frontend/src/ +git commit -m "feat: add shadcn CSS foundation and Aurora theme definitions" +``` + +--- + +### Task 3: shadcn Component Library — Form Primitives + +**Files:** +- Create: `frontend/src/components/ui/button.tsx` +- Create: `frontend/src/components/ui/input.tsx` +- Create: `frontend/src/components/ui/textarea.tsx` +- Create: `frontend/src/components/ui/select.tsx` +- Create: `frontend/src/components/ui/checkbox.tsx` +- Create: `frontend/src/components/ui/switch.tsx` +- Create: `frontend/src/components/ui/label.tsx` + +- [ ] **Step 1: Add components via shadcn CLI** + +```bash +cd frontend +npx shadcn@latest add button input textarea select checkbox switch label +``` + +This generates components in `src/components/ui/`. Each uses Radix primitives + CVA + `cn()`. + +- [ ] **Step 2: Verify button matches shadcn-template** + +Compare generated `button.tsx` with `shadcn-template/src/components/ui/button.tsx`. The shadcn-template version has Aurora-specific customizations (rounded-4xl, icon sizes). If the generated version differs significantly, replace with the template version. + +- [ ] **Step 3: Verify all components render** + +Create a temporary test: import each component in `App.jsx` (or a scratch file), render them, and confirm no build errors. + +```bash +cd frontend && npm run build +``` + +- [ ] **Step 4: Commit** + +```bash +git add frontend/src/components/ +git commit -m "feat: add shadcn form primitive components" +``` + +--- + +### Task 4: shadcn Component Library — Feedback + Layout + +**Files:** +- Create: `frontend/src/components/ui/badge.tsx` +- Create: `frontend/src/components/ui/alert.tsx` +- Create: `frontend/src/components/ui/progress.tsx` +- Create: `frontend/src/components/ui/skeleton.tsx` +- Create: `frontend/src/components/ui/tooltip.tsx` +- Create: `frontend/src/components/ui/card.tsx` +- Create: `frontend/src/components/ui/separator.tsx` +- Create: `frontend/src/components/ui/scroll-area.tsx` +- Create: `frontend/src/components/ui/table.tsx` +- Create: `frontend/src/components/ui/tabs.tsx` + +- [ ] **Step 1: Add components via shadcn CLI** + +```bash +cd frontend +npx shadcn@latest add badge alert progress skeleton tooltip card separator scroll-area table tabs +``` + +- [ ] **Step 2: Build check** + +```bash +cd frontend && npm run build +``` + +- [ ] **Step 3: Commit** + +```bash +git add frontend/src/components/ +git commit -m "feat: add shadcn feedback and layout components" +``` + +--- + +### Task 5: shadcn Component Library — Overlay Components + +**Files:** +- Create: `frontend/src/components/ui/dialog.tsx` +- Create: `frontend/src/components/ui/sheet.tsx` +- Create: `frontend/src/components/ui/dropdown-menu.tsx` + +- [ ] **Step 1: Add components via shadcn CLI** + +```bash +cd frontend +npx shadcn@latest add dialog sheet dropdown-menu +``` + +- [ ] **Step 2: Build check** + +```bash +cd frontend && npm run build +``` + +- [ ] **Step 3: Commit** + +```bash +git add frontend/src/components/ +git commit -m "feat: add shadcn overlay components (dialog, sheet, dropdown-menu)" +``` + +--- + +### Task 6: React 19 Compatibility Fixes + +**Files:** +- Modify: `frontend/package.json` +- Modify: files importing `react-helmet` + +- [ ] **Step 1: Replace react-helmet with react-helmet-async** + +`react-helmet-async` was already installed in Task 1. Remove `react-helmet`: + +```bash +cd frontend && npm uninstall react-helmet +``` + +- [ ] **Step 2: Update imports** + +Find all files importing `react-helmet`: + +```bash +cd frontend && grep -r "react-helmet" src/ --include="*.jsx" --include="*.tsx" -l +``` + +In each file, replace: +```javascript +// Before +import { Helmet } from 'react-helmet'; +// After +import { Helmet } from 'react-helmet-async'; +``` + +In `main.jsx` (or `main.tsx` after conversion), wrap the app with `HelmetProvider`: +```jsx +import { HelmetProvider } from 'react-helmet-async'; +// Wrap around the app tree + + + +``` + +- [ ] **Step 3: Audit react-loading** + +```bash +cd frontend && grep -r "react-loading" src/ --include="*.jsx" --include="*.tsx" -l +``` + +Check if `react-loading` works with React 19. If it fails, replace with a simple CSS spinner or shadcn `Skeleton` component. + +- [ ] **Step 4: Build + verify** + +```bash +cd frontend && npm run build +``` + +- [ ] **Step 5: Commit** + +```bash +git add frontend/ +git commit -m "feat: replace react-helmet with react-helmet-async for React 19" +``` + +--- + +## Phase 1 — Core Shell + Shared Infrastructure + +### Task 7: Convert Core Atoms to TypeScript + +**Files:** +- Rename: `frontend/src/atoms/auth.js` → `auth.ts` +- Rename: `frontend/src/atoms/theme.js` → `theme.ts` +- Rename: `frontend/src/atoms/layout.js` → `layout.ts` +- Rename: `frontend/src/atoms/notification.js` → `notification.ts` +- Rename: `frontend/src/atoms/notificationManager.js` → `notificationManager.ts` +- Rename: `frontend/src/atoms/modal.js` → `modal.ts` +- Rename: `frontend/src/atoms/server/limit.js` → `limit.ts` (if exists) + +- [ ] **Step 1: Convert auth.ts** + +Rename file. Add types for auth state and actions: + +```typescript +interface AuthState { + token: string | null; + permissions: { + is_superuser?: boolean; + is_ops?: boolean; + user_id?: number; + }; +} + +type AuthAction = + | { type: 'login'; token: string } + | { type: 'logout' }; +``` + +Type the `authAtom` (atomWithStorage) and `useAuthReducer` hook. Keep the JWT decode logic. + +- [ ] **Step 2: Convert remaining atoms** + +For each atom file: +1. Rename `.js` → `.ts` +2. Add explicit types for state shape +3. Add types for action/reducer parameters +4. Type the exported hooks + +`theme.ts` is trivial (4 lines). `layout.ts` is trivial (3 lines). `notification.ts` needs types for notification objects. `modal.ts` needs types for the modal stack entries and the `useModal()` hook return type. + +- [ ] **Step 3: Fix import paths in consumers** + +Since we renamed `.js` → `.ts`, imports like `from '@/atoms/auth'` should still resolve (bundler resolves without extension). Verify no explicit `.js` extensions in imports. + +- [ ] **Step 4: Build check** + +```bash +cd frontend && npm run build +``` + +- [ ] **Step 5: Commit** + +```bash +git add frontend/src/atoms/ +git commit -m "feat: convert core atoms to TypeScript" +``` + +--- + +### Task 8: Migrate Notification System from Redux to Jotai + +**Files:** +- Modify: `frontend/src/atoms/notification.ts` +- Modify: `frontend/src/graphql.js` (uses notification for error handling) +- Modify: any file importing `showNotification` from Redux +- Reference: `frontend/src/store/reducers/notification.js` (to understand API) + +- [ ] **Step 1: Audit Redux notification consumers** + +```bash +cd frontend && grep -r "showNotification\|notification.*dispatch\|store/reducers/notification" src/ --include="*.jsx" --include="*.tsx" --include="*.js" --include="*.ts" -l +``` + +Document each consumer and what it does. + +- [ ] **Step 2: Ensure Jotai notification atom has equivalent API** + +The existing `atoms/notification.ts` already has `notify()` function and `useNotificationsReducer()`. Verify it supports the same operations as the Redux version: +- `addNotification({title, body, type, duration})` +- `removeNotification(id)` +- Auto-dismiss with timeout + +If `notify()` is a standalone function (not requiring React context), it can replace `dispatch(showNotification(...))` anywhere. + +- [ ] **Step 3: Replace Redux notification imports** + +In each consumer file, replace: +```javascript +// Before +import { showNotification } from '@/store/reducers/notification'; +dispatch(showNotification({ title, body, type })); + +// After +import { notify } from '@/atoms/notification'; +notify({ title, body, type }); +``` + +Pay special attention to `graphql.js` error handler — it uses notification for GraphQL errors. + +Also check `store/reducers/utils.js` (`handleError`) — this is the centralized error handler. Replace its Redux dispatch with Jotai notify. + +- [ ] **Step 4: Build check** + +```bash +cd frontend && npm run build +``` + +- [ ] **Step 5: Commit** + +```bash +git add frontend/src/ +git commit -m "feat: migrate notification system from Redux to Jotai" +``` + +--- + +### Task 9: Relocate Generated Types + GraphQL Codegen + +**Files:** +- Create: `frontend/src/types/generated.ts` +- Modify: files importing from `store/apis/types.generated.ts` +- Modify: GraphQL codegen config (if exists) + +- [ ] **Step 1: Read codegen config** + +The codegen config is at `frontend/codegen.yml`. Read it to understand current plugins and paths. + +- [ ] **Step 2: Copy types to new location** + +```bash +mkdir -p frontend/src/types +cp frontend/src/store/apis/types.generated.ts frontend/src/types/generated.ts +``` + +- [ ] **Step 3: Update imports** + +Find all consumers: +```bash +cd frontend && grep -r "store/apis/types" src/ -l +``` + +Replace import paths: +```typescript +// Before +import { FileTypeEnum } from '../../store/apis/types.generated'; +// After +import { FileTypeEnum } from '@/types/generated'; +``` + +- [ ] **Step 4: Update codegen.yml** + +In `frontend/codegen.yml`: +- Change output path from `src/store/apis/types.generated.ts` to `src/types/generated.ts` +- Update `documents` glob from `'src/**/*.jsx'` to `'src/**/*.{jsx,tsx,ts}'` +- Remove `typescript-rtk-query` plugin (RTK Query codegen) +- Remove `importBaseApiFrom` reference to `src/store/graphqlBaseApi` +- Keep the base `typescript` and `typescript-operations` plugins + +Also update any `queries/*.js` files that import from the old `store/apis/types.generated.ts` path to use `@/types/generated`. + +- [ ] **Step 5: Build check + commit** + +```bash +cd frontend && npm run build +git add frontend/src/types/ frontend/src/ +git commit -m "feat: relocate generated GraphQL types to src/types/" +``` + +--- + +### Task 10: Theme Provider + Theme System + +**Files:** +- Create: `frontend/src/components/theme-provider.tsx` +- Modify: `frontend/src/atoms/theme.ts` +- Modify: `frontend/src/contexts/ThemeContext.jsx` (to be replaced) +- Modify: `frontend/src/main.jsx` + +- [ ] **Step 1: Define theme configuration** + +Add to `frontend/src/atoms/theme.ts`: + +```typescript +export interface ThemeConfig { + name: string; + label: string; + colorScheme: 'light' | 'dark'; +} + +export const THEMES: ThemeConfig[] = [ + { name: 'aurora-classic', label: 'Aurora Classic', colorScheme: 'light' }, + { name: 'morning', label: 'Morning', colorScheme: 'light' }, + { name: 'sunset', label: 'Sunset', colorScheme: 'dark' }, + { name: 'midnight', label: 'Midnight', colorScheme: 'dark' }, +]; + +export const DEFAULT_LIGHT_THEME = 'aurora-classic'; +export const DEFAULT_DARK_THEME = 'sunset'; +``` + +- [ ] **Step 2: Create new ThemeProvider** + +`frontend/src/components/theme-provider.tsx`: + +Based on `shadcn-template/src/components/theme-provider.tsx` but extended for named themes: +- Theme type: `'auto' | 'aurora-classic' | 'morning' | 'sunset' | 'midnight'` +- Instead of toggling `light`/`dark` classes, apply `.theme-{name}` class + `.dark` for dark themes +- `auto` resolves via `prefers-color-scheme` → `aurora-classic` or `sunset` +- Keep keyboard `d` shortcut (toggle between last-used light and dark theme) +- Keep `disableTransitionsTemporarily()` from template +- Persist to localStorage via Jotai `themeSelectionAtom` + +Key logic for applying theme: +```typescript +const config = THEMES.find(t => t.name === resolvedTheme); +root.className = ''; // clear all theme classes +root.classList.add(`theme-${resolvedTheme}`); +if (config?.colorScheme === 'dark') { + root.classList.add('dark'); +} +``` + +- [ ] **Step 3: Replace ThemeContext in main.jsx** + +Replace `import { ThemeProvider } from '@/contexts/ThemeContext'` with the new provider. Remove old `ThemeContext.jsx` file. + +- [ ] **Step 4: Delete old theme files** + +- Delete `frontend/src/contexts/ThemeContext.jsx` +- Delete `frontend/src/utils/themes.js` (generated DaisyUI theme list — no longer needed) +- Delete `frontend/scripts/generate-themes.mjs` (DaisyUI theme generator) + +- [ ] **Step 5: Build check + commit** + +```bash +cd frontend && npm run build +git add frontend/src/ +git commit -m "feat: implement multi-theme system with shadcn CSS variables" +``` + +--- + +### Task 11: Layout Shell Migration + +**Files:** +- Rename+Rewrite: `frontend/src/Layout.jsx` → `Layout.tsx` +- Rename+Rewrite: `frontend/src/features/layout/NavBar.jsx` → `NavBar.tsx` +- Rename+Rewrite: `frontend/src/features/layout/SideBar.jsx` → `SideBar.tsx` +- Rename: `frontend/src/routes.js` → `routes.ts` +- Rename+Convert: `frontend/src/features/layout/Error.jsx` → `Error.tsx` +- Rename+Convert: `frontend/src/features/layout/NoMatch.jsx` → `NoMatch.tsx` +- Rename+Convert: `frontend/src/features/layout/Hero.jsx` → `Hero.tsx` +- Delete or Rewrite: `frontend/src/features/layout/Themes.jsx` (10.5K DaisyUI theme demo page — likely obsolete with new theme system; delete if unused, or rewrite as a theme preview page) +- Delete: `frontend/src/features/layout/Footer.jsx` (empty file) + +- [ ] **Step 1: Convert routes.ts** + +Rename `routes.js` → `routes.ts`. Add type for route config: + +```typescript +interface RouteConfig { + path: string; + label: string; + icon: React.ComponentType; + adminOnly?: boolean; +} +``` + +- [ ] **Step 2: Rewrite Layout.tsx** + +Convert from DaisyUI drawer pattern to a custom AppShell using Tailwind + shadcn `Sheet`: + +- Desktop: fixed sidebar (w-60) + content area +- Mobile: `Sheet` (side=left) triggered by hamburger button +- Remove `initializeWebSocket` / `closeWebSocket` imports (legacy Redux websocket) +- Keep token refresh on mount and auth redirect logic +- Replace `classnames` imports with `cn` from `@/lib/utils` + +DaisyUI classes to remove: `drawer`, `drawer-toggle`, `drawer-open`, `drawer-content`, `drawer-side` + +Replace with Tailwind flex layout: +```tsx +
+ {/* Desktop sidebar */} + + {/* Mobile sidebar via Sheet */} + + + + + + {/* Main content */} +
+ +
+ +
+
+
+``` + +- [ ] **Step 3: Rewrite NavBar.tsx** + +Replace DaisyUI `navbar` with Tailwind flex layout: +- Sticky top bar with `bg-background border-b border-border` +- Mobile: hamburger button (opens Sheet from Layout) +- Right side: theme switch, language switch, account dropdown (using shadcn `DropdownMenu`) +- Replace DaisyUI avatar dropdown with `DropdownMenu` + `DropdownMenuTrigger` + `DropdownMenuContent` +- Replace `classnames` with `cn` + +- [ ] **Step 4: Rewrite SideBar.tsx** + +Replace DaisyUI `menu` with custom nav: +- Logo/brand at top +- Nav links from `routes.ts` config +- Active state: `bg-primary/10 text-primary` (keep current style but without DaisyUI classes) +- Use `NavLink` from react-router-dom with `cn()` for conditional active styling +- Replace `classnames` with `cn` + +- [ ] **Step 5: Build check + commit** + +```bash +cd frontend && npm run build +git add frontend/src/ +git commit -m "feat: migrate Layout, NavBar, SideBar to shadcn/Tailwind" +``` + +--- + +### Task 12: Modal System Rewrite + +**Files:** +- Rename+Rewrite: `frontend/src/features/modal/ModalManager.jsx` → `ModalManager.tsx` +- Rename+Rewrite: `frontend/src/features/ui/ModalShell.jsx` → `ModalShell.tsx` +- Rename+Rewrite: `frontend/src/features/modal/ConfirmationModal.jsx` → `ConfirmationModal.tsx` + +- [ ] **Step 1: Rewrite ModalShell.tsx** + +Replace DaisyUI `modal-box` with shadcn `Dialog`: + +```tsx +import { + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; + +interface ModalShellProps { + title: string; + onClose: () => void; + footer?: React.ReactNode; + children: React.ReactNode; + className?: string; +} + +export function ModalShell({ title, onClose, footer, children, className }: ModalShellProps) { + return ( + <> + + {title} + +
+ {children} +
+ {footer && {footer}} + + ); +} +``` + +- [ ] **Step 2: Rewrite ModalManager.tsx** + +Keep the Jotai modal atom stack system. Wrap each modal in shadcn `Dialog`: + +```tsx +import { Dialog, DialogContent } from '@/components/ui/dialog'; + +// For each modal in the stack: + { if (!open) close(modal.id); }} +> + { + if (!isTop) e.preventDefault(); + }} + > + + + +``` + +Keep the MODAL_REGISTRY pattern. Keep Esc key handling (Radix Dialog handles this by default). + +- [ ] **Step 3: Rewrite ConfirmationModal.tsx** + +Type the props and use shadcn `Button`: + +```tsx +interface ConfirmationModalProps { + title?: string; + message?: string; + confirmText?: string; + cancelText?: string; + resolve: (value: boolean) => void; + close: () => void; +} +``` + +- [ ] **Step 4: Build check + commit** + +```bash +cd frontend && npm run build +git add frontend/src/features/modal/ frontend/src/features/ui/ +git commit -m "feat: rewrite modal system with shadcn Dialog" +``` + +--- + +### Task 13: Theme Switcher + Language Switcher Migration + +**Files:** +- Rename+Rewrite: `frontend/src/features/theme/ThemeSwitch.jsx` → `ThemeSwitch.tsx` +- Rename+Rewrite: `frontend/src/features/theme/ThemeMenuItems.jsx` → `ThemeMenuItems.tsx` +- Rename+Rewrite: `frontend/src/features/i18n/LanguageSwitch.jsx` → `LanguageSwitch.tsx` +- Rename+Rewrite: `frontend/src/features/i18n/LanguageMenuItems.jsx` → `LanguageMenuItems.tsx` +- Rename: `frontend/src/i18n.js` → `i18n.ts` + +- [ ] **Step 1: Rewrite ThemeSwitch + ThemeMenuItems** + +Replace DaisyUI `dropdown` with shadcn `DropdownMenu`: + +```tsx +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { Button } from '@/components/ui/button'; +import { Palette, Check } from 'lucide-react'; +import { THEMES } from '@/atoms/theme'; +import { useTheme } from '@/components/theme-provider'; + +export function ThemeSwitch() { + const { theme, setTheme } = useTheme(); + return ( + + + + + + setTheme('auto')}> + Auto {theme === 'auto' && } + + {THEMES.map(t => ( + setTheme(t.name)}> + {t.label} {theme === t.name && } + + ))} + + + ); +} +``` + +- [ ] **Step 2: Rewrite LanguageSwitch + LanguageMenuItems** + +Same pattern with `DropdownMenu`. Replace `
` dropdown with Radix. + +- [ ] **Step 3: Convert i18n.ts** + +Rename `i18n.js` → `i18n.ts`. Minimal changes — add type imports if needed. + +- [ ] **Step 4: Build check + commit** + +```bash +cd frontend && npm run build +git add frontend/src/features/theme/ frontend/src/features/i18n/ frontend/src/i18n.ts +git commit -m "feat: migrate theme and language switchers to shadcn" +``` + +--- + +### Task 14: Notification + Paginator + Shared UI Migration + +**Files:** +- Rename+Rewrite: `frontend/src/features/Notification.jsx` → `Notification.tsx` +- Rename+Rewrite: `frontend/src/features/Paginator.jsx` → `Paginator.tsx` (if exists) +- Rename+Rewrite: `frontend/src/features/ui/PageHeader.jsx` → `PageHeader.tsx` +- Rename+Rewrite: `frontend/src/features/ui/EmptyState.jsx` → `EmptyState.tsx` +- Rename+Rewrite: `frontend/src/features/ui/dropdown/Dropdown.jsx` → removed (replaced by shadcn DropdownMenu) + +- [ ] **Step 1: Rewrite Notification.tsx** + +Keep Framer Motion animations. Replace DaisyUI alert classes with shadcn `Alert` or custom Tailwind styling: + +DaisyUI `alert-success` → shadcn `bg-green-50 text-green-900 dark:bg-green-950 dark:text-green-100` (or use Alert component with variant). + +Keep: fixed positioning, progress bar, click-to-copy, auto-dismiss, pause on hover. + +- [ ] **Step 2: Rewrite PageHeader.tsx** + +Simple — replace any DaisyUI classes with Tailwind utilities. Add TypeScript props interface. + +- [ ] **Step 3: Rewrite EmptyState.tsx** + +Simple — type the props, replace DaisyUI classes. + +- [ ] **Step 4: Migrate Paginator if it exists** + +Replace DaisyUI `btn-group` / pagination classes with shadcn `Button` variants. + +- [ ] **Step 5: Delete old Dropdown component** + +`features/ui/dropdown/Dropdown.jsx` and `DropdownSubmenu.jsx` are replaced by shadcn `DropdownMenu`. Delete them. Update any remaining imports to use shadcn. + +- [ ] **Step 6: Build check + commit** + +```bash +cd frontend && npm run build +git add frontend/src/features/ +git commit -m "feat: migrate Notification, PageHeader, EmptyState to shadcn" +``` + +--- + +### Task 15: Entry Point Migration + +**Files:** +- Rename+Rewrite: `frontend/src/main.jsx` → `main.tsx` +- Rename+Rewrite: `frontend/src/App.jsx` → `App.tsx` +- Modify: `frontend/index.html` (update script src) + +- [ ] **Step 1: Convert main.tsx** + +Rename `main.jsx` → `main.tsx`. Key changes: +- Add `HelmetProvider` wrapper (from react-helmet-async) +- Replace old `ThemeProvider` import with new one from `@/components/theme-provider` +- Keep Redux `Provider` + `PersistGate` for now (still needed by unmigrated features) +- Keep Apollo `ApolloProvider` +- Keep Sentry setup +- Update `index.html` script src from `main.jsx` to `main.tsx` + +- [ ] **Step 2: Convert App.tsx** + +Rename `App.jsx` → `App.tsx`. Key changes: +- Replace `react-helmet` imports with `react-helmet-async` +- Type the lazy route imports +- Replace `classnames` with `cn` if used +- Keep all route definitions + +- [ ] **Step 3: Convert shared infrastructure files** + +Rename and add minimal types: +- `graphql.js` → `graphql.ts` — type the Apollo client config +- `routes.js` → `routes.ts` — already done in Task 11 if not + +- [ ] **Step 4: Build check + commit** + +```bash +cd frontend && npm run build +git add frontend/src/ frontend/index.html +git commit -m "feat: convert entry points (main, App) to TypeScript" +``` + +--- + +## Phase 2 — Auth + Simple Features + +### Task 16: Auth Feature Migration + +**Files:** +- Rename+Rewrite: `frontend/src/features/auth/Login.jsx` → `Login.tsx` +- Rename+Rewrite: `frontend/src/features/auth/CreateAccoount.jsx` → `CreateAccount.tsx` (fix typo in filename) +- Rename+Rewrite: `frontend/src/features/auth/EmailPasswordForm.jsx` → `EmailPasswordForm.tsx` +- Convert: `frontend/src/apis/auth.js` → `auth.ts` + +- [ ] **Step 1: Convert apis/auth.ts** + +Rename, add types for request/response shapes. Keep axios calls. + +- [ ] **Step 2: Migrate auth components** + +For each component: +1. Rename `.jsx` → `.tsx` +2. Replace `classnames` with `cn` +3. Replace DaisyUI form classes: `input input-bordered` → shadcn `Input`, `btn btn-primary` → shadcn `Button` +4. Replace DaisyUI `card` with shadcn `Card` +5. Add TypeScript interfaces for props +6. Replace any Redux auth imports with Jotai `useAuthReducer` + +- [ ] **Step 3: Build check + commit** + +```bash +cd frontend && npm run build +git add frontend/src/features/auth/ frontend/src/apis/ +git commit -m "feat: migrate auth feature to shadcn + TypeScript" +``` + +--- + +### Task 17: About + User Feature Migration + +**Files:** +- Rename+Rewrite: `frontend/src/features/about/About.jsx` → `About.tsx` +- Rename+Rewrite: `frontend/src/features/user/Users.jsx` → `Users.tsx` +- Rename+Rewrite: `frontend/src/features/user/ServerUsers.jsx` → `ServerUsers.tsx` +- Convert: `frontend/src/apis/users.js` → `users.ts` + +- [ ] **Step 1: Migrate About page** + +Trivial — rename, add types, replace DaisyUI classes with Tailwind/shadcn equivalents. + +- [ ] **Step 2: Migrate User components** + +Replace DaisyUI `table` classes with shadcn `Table`. Replace `badge` with shadcn `Badge`. Replace `modal` patterns with shadcn `Dialog`. + +- [ ] **Step 3: Convert apis/users.ts** + +Rename, add request/response types. + +- [ ] **Step 4: Build check + commit** + +```bash +cd frontend && npm run build +git add frontend/src/features/about/ frontend/src/features/user/ frontend/src/apis/users.ts +git commit -m "feat: migrate About and User features to shadcn + TypeScript" +``` + +--- + +## Phase 3 — Core Business Features + +### Task 18: Server Feature Migration + +**Files:** +- All files in `frontend/src/features/server/` — rename `.jsx` → `.tsx`, rewrite +- Convert: `frontend/src/apis/servers.js` → `servers.ts` +- Convert: `frontend/src/hooks/useServerItem.js` → `useServerItem.ts` +- Convert: `frontend/src/hooks/useServerMetrics.js` → `useServerMetrics.ts` + +- [ ] **Step 1: Convert server hooks** + +`useServerItem.ts` and `useServerMetrics.ts` — rename, add types for return values and parameters. + +- [ ] **Step 2: Migrate server components** + +Key replacements: +- `ServerCard` / `ServerRow`: DaisyUI card classes → shadcn `Card` +- `ServerStat` / `ServerPortsStat`: DaisyUI `stats` / `stat` → custom Tailwind or shadcn Card sections +- `ServerInfoModal`: DaisyUI modal → uses ModalShell (already migrated) +- Charts (`Chart.jsx`, `Sparkline.jsx`): Recharts stays, just update wrapper styling +- `classnames` → `cn` in all files +- Any Redux imports → audit and replace or document as TODO + +- [ ] **Step 3: Convert apis/servers.ts** + +Rename, add request/response types. + +- [ ] **Step 4: Build check + commit** + +```bash +cd frontend && npm run build +git add frontend/src/features/server/ frontend/src/apis/servers.ts frontend/src/hooks/ +git commit -m "feat: migrate server feature to shadcn + TypeScript" +``` + +--- + +### Task 19: Port Feature Migration + +**Files:** +- All files in `frontend/src/features/port/` — rename `.jsx` → `.tsx`, rewrite +- Convert: `frontend/src/apis/ports.js` → `ports.ts` + +- [ ] **Step 1: Migrate port components** + +- `PortCard` / `PortSelectCard`: DaisyUI card → shadcn `Card` +- `PortFunctionModal` / `PortRestrictionModal`: already use ModalShell (migrated) +- **Redux dependencies:** `PortRestrictionModal.jsx` uses `useSelector` and `PortCard.jsx` uses `useDispatch`. Replace with Jotai atoms or Apollo queries. If the data isn't available via GraphQL yet, document as TODO. +- Include subdirectories: `function/`, `restriction/` (contains `PortExpiration.jsx`), `PortUsersCard.jsx`, `ServerPorts.jsx` +- Form inputs: DaisyUI `input` / `select` → shadcn `Input` / `Select` +- `classnames` → `cn` + +- [ ] **Step 2: Convert apis/ports.ts** + +Rename, add types. + +- [ ] **Step 3: Build check + commit** + +```bash +cd frontend && npm run build +git add frontend/src/features/port/ frontend/src/apis/ports.ts +git commit -m "feat: migrate port feature to shadcn + TypeScript" +``` + +--- + +### Task 20: File Feature Migration + +**Files:** +- All files in `frontend/src/features/file/` — rename `.jsx` → `.tsx`, rewrite + +- [ ] **Step 1: Migrate file components** + +- `FileCard` / `FileRow`: DaisyUI styling → Tailwind + shadcn Card +- `FileModal` / `FilePreviewModal`: uses ModalShell (migrated) +- Update `FileTypeEnum` import to `@/types/generated` +- `classnames` → `cn` + +- [ ] **Step 2: Build check + commit** + +```bash +cd frontend && npm run build +git add frontend/src/features/file/ +git commit -m "feat: migrate file feature to shadcn + TypeScript" +``` + +--- + +## Phase 4 — Complex Features + +### Task 21: Deployment Feature Migration + +**Files:** +- All files in `frontend/src/features/deployment/` — rename `.jsx` → `.tsx`, rewrite + +- [ ] **Step 1: Migrate deployment components** + +- `DeployModal`: Uses `useDynamicForm` + `serviceDefinitionToDynamicSchema` — keep this integration, just restyle the container with shadcn `Dialog` +- `DeploymentList`: DaisyUI table → shadcn `Table` +- `DeploymentDetailModal`: ModalShell (migrated) +- `DeploymentStatusBadge`: DaisyUI badge → shadcn `Badge` +- `BindingModal`: ModalShell + form inputs → shadcn equivalents +- `classnames` → `cn` + +- [ ] **Step 2: Build check + commit** + +```bash +cd frontend && npm run build +git add frontend/src/features/deployment/ +git commit -m "feat: migrate deployment feature to shadcn + TypeScript" +``` + +--- + +### Task 22: Service Editor — Field Components Migration + +**Files:** +- Rename+Rewrite: `frontend/src/features/service-editor/fields/FieldShell.jsx` → `FieldShell.tsx` +- Rename+Rewrite: `frontend/src/features/service-editor/fields/FieldsRenderer.jsx` → `FieldsRenderer.tsx` +- Rename+Rewrite: `frontend/src/features/service-editor/fields/TextField.jsx` → `TextField.tsx` +- Rename+Rewrite: `frontend/src/features/service-editor/fields/SelectField.jsx` → `SelectField.tsx` +- Rename+Rewrite: `frontend/src/features/service-editor/fields/CheckboxField.jsx` → `CheckboxField.tsx` +- Rename+Rewrite: `frontend/src/features/service-editor/fields/TextAreaField.jsx` → `TextAreaField.tsx` +- Rename+Rewrite: `frontend/src/features/service-editor/fields/ListField.jsx` → `ListField.tsx` +- Rename+Rewrite: `frontend/src/features/service-editor/fields/ObjectField.jsx` → `ObjectField.tsx` +- Rename+Rewrite: `frontend/src/features/service-editor/fields/FieldError.jsx` → `FieldError.tsx` + +- [ ] **Step 1: Rewrite FieldShell.tsx** + +Replace DaisyUI form-control pattern with shadcn `Label` + error display: + +```tsx +import { Label } from '@/components/ui/label'; + +interface FieldShellProps { + label?: string; + error?: string; + required?: boolean; + children: React.ReactNode; + className?: string; +} + +export function FieldShell({ label, error, required, children, className }: FieldShellProps) { + return ( +
+ {label && ( + + )} + {children} + {error &&

{error}

} +
+ ); +} +``` + +- [ ] **Step 2: Migrate individual field components** + +For each field: +- `TextField`: DaisyUI `input input-bordered` → shadcn `Input` +- `SelectField`: DaisyUI `select select-bordered` → shadcn `Select` +- `CheckboxField`: DaisyUI `checkbox` → shadcn `Checkbox` +- `TextAreaField`: DaisyUI `textarea textarea-bordered` → shadcn `Textarea` +- `ListField`: keep array logic, restyle add/remove buttons with shadcn `Button` +- `ObjectField`: keep recursive rendering, restyle container + +All get TypeScript interfaces. All replace `classnames` with `cn`. + +- [ ] **Step 3: Migrate FieldsRenderer.tsx** + +Type the schema and field dispatch logic. Keep recursive rendering for nested objects/arrays. + +- [ ] **Step 4: Build check + commit** + +```bash +cd frontend && npm run build +git add frontend/src/features/service-editor/fields/ +git commit -m "feat: migrate service editor field components to shadcn + TypeScript" +``` + +--- + +### Task 23: Service Editor — ParamEditorPanel Refactor + Migration + +**Files:** +- Rename+Rewrite+Split: `frontend/src/features/service-editor/ParamEditorPanel.jsx` → multiple `.tsx` files +- Create: `frontend/src/features/service-editor/ParamList.tsx` +- Create: `frontend/src/features/service-editor/EmitConfigEditor.tsx` +- Create: `frontend/src/features/service-editor/ValidationEditor.tsx` +- Create: `frontend/src/features/service-editor/ConditionEditor.tsx` +- Create: `frontend/src/features/service-editor/UIConfigEditor.tsx` +- Create: `frontend/src/features/service-editor/ParamTypeEditor.tsx` + +- [ ] **Step 1: Read and understand ParamEditorPanel.jsx** + +Read the full 1114-line file. Identify the logical sections: +1. Param list (left sidebar with add/remove/reorder) +2. Param type selection + type-specific options +3. Emit configuration (arg, flag, env, file, stdin, pos) +4. Validation rules +5. Conditional visibility +6. UI config hints + +- [ ] **Step 2: Extract ParamList.tsx** + +Extract the param list sidebar into its own component. Props: +```typescript +interface ParamListProps { + params: ParamDraft[]; + selectedIndex: number; + onSelect: (index: number) => void; + onAdd: () => void; + onRemove: (index: number) => void; + onReorder: (fromIndex: number, toIndex: number) => void; +} +``` + +- [ ] **Step 3: Extract EmitConfigEditor.tsx** + +Extract emit target configuration. Props: +```typescript +interface EmitConfigEditorProps { + emit: EmitConfig; + paramType: string; + onChange: (emit: EmitConfig) => void; +} +``` + +- [ ] **Step 4: Extract ValidationEditor.tsx, ConditionEditor.tsx, UIConfigEditor.tsx, ParamTypeEditor.tsx** + +Each gets its own file with typed props and `onChange` callback pattern. + +- [ ] **Step 5: Rewrite ParamEditorPanel.tsx as shell** + +The shell renders `ParamList` on the left, and the selected param's editors on the right (organized in Tabs using shadcn `Tabs`): +- Tab 1: Type + Basic (ParamTypeEditor) +- Tab 2: Emit (EmitConfigEditor) +- Tab 3: Validation (ValidationEditor) +- Tab 4: Conditions (ConditionEditor) +- Tab 5: UI (UIConfigEditor) + +- [ ] **Step 6: Build check + commit** + +```bash +cd frontend && npm run build +git add frontend/src/features/service-editor/ +git commit -m "feat: refactor and migrate ParamEditorPanel to shadcn + TypeScript" +``` + +--- + +### Task 24: Service Editor — Remaining Files Migration + +**Files:** +- Rename+Rewrite: `frontend/src/features/service-editor/ServiceEditorPage.jsx` → `ServiceEditorPage.tsx` +- Rename+Rewrite: `frontend/src/features/service-editor/ServiceListPage.jsx` → `ServiceListPage.tsx` +- Rename+Rewrite: `frontend/src/features/service-editor/AuthoringJsonPanel.jsx` → `AuthoringJsonPanel.tsx` +- Rename+Rewrite: `frontend/src/features/service-editor/FormPreviewPanel.jsx` → `FormPreviewPanel.tsx` +- Rename+Rewrite: `frontend/src/features/service-editor/CompileOutputPanel.jsx` → `CompileOutputPanel.tsx` +- Rename+Rewrite: `frontend/src/features/service-editor/useDynamicForm.jsx` → `useDynamicForm.tsx` +- Rename+Rewrite: `frontend/src/features/service-editor/serviceAdapter.js` → `serviceAdapter.ts` + +- [ ] **Step 1: Convert serviceAdapter.ts** + +Rename, add types for the param→fieldSpec mapping. Key types: +```typescript +interface FieldSpec { + type: 'text' | 'number' | 'checkbox' | 'select' | 'password' | 'textarea' | 'list' | 'object'; + label?: string; + required?: boolean; + default?: unknown; + options?: { value: string; label: string }[]; + validation?: Record; + grid?: Record; + children?: Record; +} +``` + +- [ ] **Step 2: Convert useDynamicForm.tsx** + +Rename, type the hook parameters and return value. Keep `react-hook-form` integration. + +- [ ] **Step 3: Migrate panel components** + +For each panel: +- Replace DaisyUI classes with Tailwind/shadcn equivalents +- `classnames` → `cn` +- Add TypeScript interfaces + +`ServiceEditorPage` orchestrates 3 panels + `ParamEditorPanel` — update imports to new split modules. + +- [ ] **Step 4: Migrate ServiceListPage** + +Table of service definitions. DaisyUI table → shadcn `Table`. DaisyUI buttons → shadcn `Button`. + +- [ ] **Step 5: Build check + commit** + +```bash +cd frontend && npm run build +git add frontend/src/features/service-editor/ +git commit -m "feat: migrate service editor pages and adapters to shadcn + TypeScript" +``` + +--- + +## Phase 5 — Cleanup + +### Task 25: Remove DaisyUI + Redux + Dead Code + +**Files:** +- Modify: `frontend/package.json` +- Modify: `frontend/src/index.css` +- Modify: `frontend/src/main.tsx` +- Delete: `frontend/src/store/` (entire directory) +- Delete: `frontend/src/features/ui/dropdown/` (replaced by shadcn) +- Delete: `frontend/tailwind.config.cjs` (if no longer needed with Tailwind v4 CSS config) + +- [ ] **Step 1: Remove DaisyUI from CSS** + +In `frontend/src/index.css`: +- Remove `@plugin 'daisyui'` block +- Remove all `@plugin 'daisyui/theme'` blocks +- Remove DaisyUI-specific custom rules (`.menu` override, `.stat` override) +- Keep shadcn imports, theme definitions, and custom breakpoints + +- [ ] **Step 2: Remove Redux from main.tsx** + +Remove `Provider`, `PersistGate`, `store`, `persistor` imports and wrappers. + +- [ ] **Step 3: Delete store/ directory** + +```bash +rm -rf frontend/src/store/ +``` + +- [ ] **Step 4: Uninstall removed packages** + +```bash +cd frontend +npm uninstall daisyui @reduxjs/toolkit redux-persist react-redux classnames phosphor-react +``` + +- [ ] **Step 4b: Remove symbolObservable polyfill** + +Delete `frontend/src/polyfills/symbolObservable.js` and remove its import from `main.tsx`. This polyfill was needed for old Redux. + +- [ ] **Step 5: Clean up remaining .jsx files** + +```bash +cd frontend && find src/ -name "*.jsx" -type f +``` + +Any remaining `.jsx` files should be renamed to `.tsx` and converted. Also check for `.js` files that should be `.ts`. + +- [ ] **Step 6: Review vestigial config files** + +- `tailwind-safelist.js`: Check if dynamic Tailwind classes in the safelist are still needed. If DaisyUI classes were the main reason, remove the file. If custom dynamic classes remain, convert to `.ts`. +- `tailwind.config.cjs`: With Tailwind v4 CSS config in `index.css`, this file may be vestigial. Check if anything references it; remove if unused. +- `postcss.config.cjs`: Check if `@tailwindcss/postcss` is still needed or if `@tailwindcss/vite` plugin covers it. Remove if unused. +- `openapi-config.cjs`: REST API codegen config. Out of scope for this migration but note for future cleanup. + +- [ ] **Step 7: Clean up unused imports and dead code** + +Run the build and fix any warnings about unused imports. Check for any remaining `classnames` imports, `data-theme` references, or DaisyUI class usage. + +- [ ] **Step 8: Final build + smoke test** + +```bash +cd frontend && npm run build +``` + +Verify the built app renders correctly with all 4 themes. + +- [ ] **Step 9: Remove DaisyUI theme reference (optional)** + +If `src/styles/daisyui-themes-reference.css` is no longer useful, delete it. + +- [ ] **Step 10: Tighten TypeScript config** + +In `tsconfig.app.json`, set: +```json +"noUnusedLocals": true, +"noUnusedParameters": true, +"allowJs": false +``` + +Fix any resulting errors. + +- [ ] **Step 11: Commit** + +```bash +git add -A frontend/ +git commit -m "feat: remove DaisyUI, Redux, and legacy code — migration complete" +``` + +- [ ] **Step 12: Convert remaining shared files** + +Convert any remaining utility/hook files not yet touched: +- `frontend/src/utils/*.js` → `.ts` +- `frontend/src/hooks/*.js` → `.ts`/`.tsx` +- `frontend/src/apis/utils.js` → `utils.ts` +- `frontend/src/queries/*.js` → `.ts` + +```bash +git add frontend/src/ +git commit -m "feat: convert remaining shared files to TypeScript" +``` diff --git a/docs/superpowers/specs/2026-03-24-design-system-foundation-design.md b/docs/superpowers/specs/2026-03-24-design-system-foundation-design.md new file mode 100644 index 0000000..bf12918 --- /dev/null +++ b/docs/superpowers/specs/2026-03-24-design-system-foundation-design.md @@ -0,0 +1,309 @@ +# Design System Foundation — Spec + +## Summary + +Establish the radix-nova design language across the Aurora frontend: global styles (font, background, typography), shared page patterns (TableCard, FormDialog, EmptyState, etc.), layout tweaks, and a full redesign of the Servers page as proof-of-concept. Subsequent sub-projects will redesign remaining pages using these patterns. + +## Decisions + +| Decision | Choice | +|---|---| +| Font | Noto Sans Variable via `@fontsource-variable/noto-sans` | +| Page background | `bg-muted` (light gray) with white cards on top | +| Density | Slightly more spacious than demo (`gap-5`, `p-5 md:p-6`) | +| Servers layout | Table-in-card (single Card wrapping a Table) | +| Forms | `Field` / `FieldGroup` / `InputGroup` everywhere | +| Proof page | Servers (most complex, exercises all patterns) | + +## 1. Global Styles + +### Font + +Install the font package: `npm install @fontsource-variable/noto-sans` + +In `index.css`, add: + +```css +@import "@fontsource-variable/noto-sans"; +``` + +In the `@theme inline` block, set: + +```css +--font-sans: 'Noto Sans Variable', sans-serif; +``` + +Remove the existing `--font-sans: var(--font-sans);` self-reference. No separate heading font — everything uses `--font-sans`. Note: `--font-heading: var(--font-sans);` also exists in the `@theme inline` block and will correctly inherit the new Noto Sans font — leave it as-is. + +### Page background + +The `SidebarInset` content area in `Layout.tsx` gets `bg-muted`. Cards (white) sit on top of the gray surface, creating visual depth. + +### Typography scale + +| Element | Current | New | +|---|---|---| +| Page titles | `text-2xl font-extrabold` | `text-lg font-semibold` | +| Section descriptions | varies | `text-sm text-muted-foreground` | +| Table body text | `text-sm` | `text-sm` (unchanged) | +| Card titles | `text-base font-medium` | `text-base font-medium` (unchanged) | + +### Spacing + +| Context | Demo | Aurora (slightly more spacious) | +|---|---|---| +| Card grid gap | `gap-4` | `gap-5` | +| Container padding | `p-4 sm:p-6` | `p-5 md:p-6` | +| Card inner padding | component default (`p-6`) | component default (unchanged) | +| Container max-width | none | `max-w-7xl` | + +## 2. Shared Page Patterns + +### PageHeader + +Simplified page header. Not wrapped in a Card — sits directly on the `bg-muted` surface. + +```tsx +
+
+

{title}

+ {description && ( +

{description}

+ )} +
+
+ {actions} +
+
+``` + +Update `features/ui/PageHeader.tsx` to match this structure. Remove any Card wrapping, large font sizes, or extrabold weights. + +### TableCard pattern + +Not a new component — a composition pattern used by every page that displays tabular data: + +```tsx + + +
+
+ {title} + {description} +
+ {headerActions} +
+
+ + ...
+
+ + {count} total + + +
+``` + +Pages implement this pattern inline — no wrapper component needed. + +### EmptyState + +Replace the current custom `EmptyState` component with the zip's `Empty` component: + +```tsx +import { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent } from "@/components/ui/empty" + + + + No servers yet + Add a server to get started. + + + + + +``` + +The `Empty` component exports: `Empty`, `EmptyHeader`, `EmptyTitle`, `EmptyDescription`, `EmptyContent`, `EmptyMedia`. There is no `EmptyActions` — use `EmptyContent` for action buttons. + +This goes inside the `CardContent` area of a TableCard when there's no data. + +### FormDialog pattern + +All modals with forms use `Field` / `FieldGroup` from the zip: + +```tsx + + + + {title} + {description} + + + + Name + + + + Address + + + + :22 + + + + + + + + + + +``` + +Replace raw `Label` + `Input` + `div` stacking with this pattern in all modals. + +### DataLoading / ThemedSuspense + +- `DataLoading.tsx` already uses `Skeleton` — keep it as-is for the 13 consumers outside scope +- The Servers page rewrite uses `Skeleton` directly for table-shaped loading states (rows of skeleton cells) +- `ThemedSuspense.tsx`: update to centered `Spinner` on `bg-muted` + +### Paginator + +Keep the current `Paginator` component as-is — it already uses valid radix-nova sizes (`size="icon-sm"` for nav buttons, `size="sm"` for Select). The TableCard pattern places it in `CardFooter`. + +## 3. Servers Page Redesign + +### Current state + +The servers page has: +- `ServerList.tsx` — parent component with table/card toggle +- `ServerRow.tsx` — table row per server +- `ServerCard.tsx` — card per server (grid mode) +- `ServerStat.tsx`, `ServerPortsStat.tsx`, `ServerTrafficStat.tsx`, `ServerSSHStat.tsx` — stat sub-components +- `ServerInfoModal.tsx` — add/edit server dialog +- `chart/Chart.tsx`, `chart/Sparkline.tsx` — chart components + +### New structure + +**ServerList.tsx** — Rewrite as a single table-in-card: + +``` +PageHeader: "Servers" / "Manage remote servers..." [+ Add Server] + +Card +├── CardContent +│ └── Table +│ ├── TableHeader: Server | SSH | Ports | Traffic | CPU | Mem | Disk | Actions +│ └── TableBody: rows... +└── CardFooter: "{n} servers" + Paginator +``` + +**Table columns:** + +| Column | Content | +|---|---| +| Server | Name (`font-medium`) + address below (`text-xs text-muted-foreground`) | +| SSH | Colored dot: green=connected, red=error, gray=unknown. Use `Badge` with dot indicator | +| Ports | `{used}/{total}` text | +| Traffic | `↑ {upload}` / `↓ {download}` compact format with `text-xs` | +| CPU | `Progress` bar (`w-16 h-1.5`) + `{value}%` text in `text-muted-foreground` | +| Mem | Same as CPU | +| Disk | Same as CPU | +| Actions | `Button` "Edit" + `DropdownMenu` trigger (three dots) for "View" (navigate to server detail), Delete | + +**Empty state:** `Empty` component inside `CardContent` when no servers. + +**Remove:** +- `ServerCard.tsx` — No longer needed (table-only view) +- `ServerRow.tsx` — Replaced by inline table rows in `ServerList.tsx` +- `ServerStat.tsx`, `ServerPortsStat.tsx`, `ServerTrafficStat.tsx`, `ServerSSHStat.tsx` — Stats are now inline Progress bars in table cells +- `framer-motion` animations in `ServerList.tsx` — The staggered card animations are removed with the card grid + +**Keep:** +- `ServerContainer.tsx` — Needed as a React Router route wrapper for nested routes (`/app/servers/:serverId`). Fix the React Router v5 `matchPath` API call to use v6 syntax: `matchPath("/app/servers", location.pathname)` instead of `matchPath({ path: "/app/servers", exact: true }, ...)`. +- `chart/Sparkline.tsx` — May be used in future stat cards, keep but not used on this page initially +- `chart/Chart.tsx` — Same +- `ServerInfoModal.tsx` — Restyle to FormDialog pattern + +### ServerInfoModal redesign + +Convert to FormDialog pattern: +- Replace raw `Label` + `Input` pairs with `Field` + `FieldLabel` + `Input` +- Use `FieldGroup` to wrap the form fields +- Use `InputGroup` where appropriate (e.g., address:port input) +- Keep the existing tabs if present (General, SSH config, etc.) +- Keep the AlertDialog for delete confirmation + +## 4. Layout Tweaks + +### SidebarInset content area + +In `Layout.tsx`, add `bg-muted` to the content wrapper: + +```diff +-
++
+``` + +### Container + +Add `max-w-7xl` to prevent content stretching on ultrawide monitors: + +```diff +-
++
+``` + +### Sidebar and navbar + +No changes. They already use the correct styling from the migration. + +## Files Changed + +### New/reinstalled packages +- `@fontsource-variable/noto-sans` + +### Modified files +- `frontend/src/index.css` — Font import + `--font-sans` variable +- `frontend/src/Layout.tsx` — `bg-muted` on content area, `max-w-7xl` container +- `frontend/src/features/ui/PageHeader.tsx` — Simplified to flex row, smaller typography +- `frontend/src/features/ui/EmptyState.tsx` — Rewrite to use `Empty` component +- `frontend/src/features/ThemedSuspense.tsx` — Centered `Spinner` on `bg-muted` +- `frontend/src/features/server/ServerList.tsx` — Full rewrite as table-in-card +- `frontend/src/features/server/ServerContainer.tsx` — Fix React Router v6 `matchPath` call +- `frontend/src/features/server/ServerInfoModal.tsx` — Restyle to FormDialog pattern + +### Deleted files +- `frontend/src/features/server/ServerCard.tsx` +- `frontend/src/features/server/ServerRow.tsx` +- `frontend/src/features/server/ServerStat.tsx` +- `frontend/src/features/server/ServerPortsStat.tsx` +- `frontend/src/features/server/ServerTrafficStat.tsx` +- `frontend/src/features/server/ServerSSHStat.tsx` +- `frontend/src/hooks/useServerItem.ts` — Logic folded into ServerList.tsx + +### Untouched +- All other pages (auth, files, services, ports, users, deployments, about) +- All GraphQL queries +- All atoms except layout-related +- `frontend/src/hooks/useServerMetrics.ts` — Not needed for the new table (which uses snapshot metrics from subscription), but kept for future server detail views +- `frontend/src/features/ui/PageSection.tsx` — Still used by other pages (files, deployments, services, ports). Will be deprecated when those pages are redesigned. +- `frontend/src/features/DataLoading.tsx` — Already uses Skeleton. Keep as-is; imported by 13 files outside scope. Servers page uses Skeleton directly for table loading state. +- `frontend/src/features/server/ServerContainer.tsx` — Route wrapper, kept with v6 matchPath fix +- Backend +- Sidebar and navbar + +## Out of Scope + +- Redesigning auth pages (Login, CreateAccount) +- Redesigning files page +- Redesigning services/service editor pages +- Redesigning ports/users pages +- Redesigning deployment pages +- Redesigning about/themes pages +- Dark mode adjustments (zinc dark palette is already set) + +These are subsequent sub-projects that will use the patterns established here. diff --git a/docs/superpowers/specs/2026-03-24-radix-nova-migration-design.md b/docs/superpowers/specs/2026-03-24-radix-nova-migration-design.md new file mode 100644 index 0000000..0352b46 --- /dev/null +++ b/docs/superpowers/specs/2026-03-24-radix-nova-migration-design.md @@ -0,0 +1,286 @@ +# Radix-Nova UI Migration Design + +## Summary + +Replace all 20 existing shadcn/ui components with the full radix-nova style component set (56 components after excluding duplicates and unused toast files), adopt radix-nova as the new design language, rebuild the layout shell on the shadcn Sidebar primitive, replace the Jotai modal manager with inline Dialog/Sheet usage, switch notifications to Sonner, and migrate all pages in waves. + +## Decisions + +| Decision | Choice | Rationale | +|---|---|---| +| Style direction | Radix-nova replaces Aurora personality | Clean break, accept new design language entirely | +| Theming | Zinc light/dark active; `.theme-*` scaffolding kept empty; ThemeProvider infrastructure preserved | Future theme addition without current maintenance burden | +| `"use client"` directives | Strip all | Vite doesn't need them; cleaner code | +| Toast/notification system | Sonner only | Modern, minimal, shadcn's direction | +| Sidebar | Adopt shadcn sidebar primitive | Replace custom SideBar.tsx + NavBar.tsx | +| Modal pattern | Inline Dialog/Sheet with local state | Replace Jotai ModalManager pattern | +| Migration strategy | Drop-and-replace, then page waves | Single-owner project; fastest path with least lingering mess | +| Migration order | Foundation → Layout → Auth → Server → Port/User → Files/Deploy → Services/Misc | Waves ordered by complexity; auth first as smoke test | + +## Section 1: Foundation — Component Drop & CSS Reset + +### Components + +Copy all `.tsx` files from the radix-nova zip `components/ui/` into `frontend/src/components/ui/`, replacing the existing 20, with these exclusions: + +- **Exclude `toast.tsx` and `toaster.tsx`** — We are using Sonner only; these are the older Radix Toast system and would create a conflicting dual-toast setup. +- **Exclude `use-mobile.tsx` and `use-toast.ts`** from `components/ui/` — These are hooks that are duplicated in the zip's `hooks/` directory. Copy the `hooks/` versions instead to avoid import confusion. + +Copy from the zip's `hooks/` directory into `src/hooks/`: +- `use-mobile.ts` + +Do NOT copy `use-toast.ts` — Sonner is the active toast system and this file would be dead code. + +This results in **56 component files** in `src/components/ui/`. + +Modifications to every component file: +- Strip `"use client"` directives +- Verify `@/lib/utils` and `@/components/ui/*` import paths match the existing Vite alias setup (they already do) + +### Sonner adaptation (CRITICAL) + +The zip's `sonner.tsx` imports `useTheme` from `next-themes`. This project uses a custom `ThemeProvider` at `@/components/theme-provider`. Rewrite the import: + +```diff +- import { useTheme } from "next-themes" ++ import { useTheme } from "@/components/theme-provider" +``` + +The custom `useTheme` returns `{ theme, resolvedTheme, setTheme }` where `resolvedTheme` is a `ThemeName` like `"aurora-classic"` or `"midnight"` — not `"light"` or `"dark"`. Derive Sonner's theme from the `THEMES` config: + +```typescript +import { useTheme } from "@/components/theme-provider" +import { THEMES } from "@/atoms/theme" + +const { resolvedTheme } = useTheme() +const themeConfig = THEMES.find((t) => t.name === resolvedTheme) +const sonnerTheme = themeConfig?.colorScheme === "dark" ? "dark" : "light" +``` + +This looks up the theme's `colorScheme` property (`"light"` or `"dark"`) which correctly handles all theme names. + +### CSS (`index.css`) + +- Adopt the zip's `app/globals.css` theme variable structure (the `@theme inline` block with `radius-sm` through `radius-4xl`) +- Replace color variables with the zip's zinc light/dark palette as the active `:root` and `.dark` defaults +- **Add `--destructive-foreground`** variable to both `:root` and `.dark` blocks — the zip defines this and several new components reference it (toast, alert-dialog, button destructive variant). Use the zip's values. +- Keep the `.theme-*` class scaffolding as empty shells (no color definitions) for future theme additions. The `ThemeProvider` infrastructure (class toggling, `'d'` keyboard shortcut, cross-tab sync, localStorage persistence) remains functional — it just applies to the single zinc light/dark pair for now. +- **`font-heading` removal strategy:** Remove the `@fontsource-variable/space-grotesk` and `@fontsource-variable/noto-sans` imports and the `--font-heading` / `--font-sans` CSS variable overrides. Then do a global find-and-replace of `font-heading` → `font-sans` across all files that use it as a Tailwind class. This affects ~15 files including `dialog.tsx`, `card.tsx`, `alert.tsx`, `sheet.tsx`, `PageHeader.tsx`, `EmptyState.tsx`, `ServerCard.tsx`, `FileCard.tsx`, and service editor panels. +- **Remove `@plugin './plugins/tailwind-mix.js'`** and `@tailwindcss/typography` — no files use the `prose` class. +- Keep `@import "shadcn/tailwind.css"` + +### Notifications + +**Sonner integration:** +- Add `` (Sonner) to the root provider stack in `main.tsx` +- Remove `features/Notification.tsx` + +**Notification migration (CRITICAL):** +The current codebase uses notifications in two ways: + +1. **Imperative `notify()` calls** (outside React) — `graphql.ts` (Apollo error link) uses the non-hook `notify()` from `atoms/notification.ts` +2. **Hook-based `useNotificationsReducer` / `addNotification` calls** (inside React) — used by `features/server/ServerRow.tsx` and `features/auth/EmailPasswordForm.tsx` + +Sonner's `toast()` function supports both patterns — it can be called imperatively from anywhere, or from within components. + +Migration steps: +- Replace all `notify({...})` calls in `graphql.ts` with `toast.error(message)` / `toast.success(message)` +- Replace all `addNotification({...})` calls in `ServerRow.tsx` and `EmailPasswordForm.tsx` with `toast()` calls +- Update `App.tsx` to remove the `` render and its lazy import (also remove `` render) +- Delete `atoms/notification.ts` +- Delete `atoms/notificationManager.ts` (older EventTarget-based system, currently unused) + +### `lib/utils.ts` + +Already has the same `cn()` function — no change needed. + +## Section 2: Layout Shell — Sidebar & Navigation + +### Remove + +- `features/layout/SideBar.tsx` (rebuilt on shadcn Sidebar primitive) +- `features/layout/NavBar.tsx` (rebuilt as a sticky header inside `SidebarInset`) +- `features/ui/ModalShell.tsx` +- `features/modal/ModalManager.tsx` +- `features/modal/ConfirmationModal.tsx` +- `features/Notification.tsx` + +### Sidebar persistence adaptation (CRITICAL) + +The zip's `sidebar.tsx` persists collapse state via `document.cookie` — this is a Next.js SSR pattern. In this Vite SPA, replace the cookie mechanism with `localStorage` (or reuse the existing `drawerOpenAtom` from `atoms/layout.ts` if appropriate). The sidebar should read initial state from localStorage on mount and write state changes back. + +### Rebuild `Layout.tsx` — Sidebar + Sticky Navbar + +The current layout pattern is **sidebar (left) + sticky top navbar (top of content area)**. This pattern is preserved using the shadcn Sidebar primitive: + +``` +┌──────────┬──────────────────────────────┐ +│ │ Sticky NavBar │ +│ Sidebar │ [SidebarTrigger] [spacer] [account dropdown] │ +│ ├──────────────────────────────┤ +│ nav │ │ +│ items │ (page content) │ +│ │ │ +│ │ │ +│ footer: │ │ +│ theme/ │ │ +│ lang │ │ +└──────────┴──────────────────────────────┘ +``` + +**Sidebar (left):** +- `SidebarProvider` + `Sidebar` + `SidebarContent` + `SidebarGroup` + `SidebarMenu` as structural skeleton +- Port existing nav items from `routes.ts` into `SidebarMenuItem` entries +- Theme switch and language switch move into `SidebarFooter` +- Mobile: shadcn sidebar handles responsive collapse natively (sheet-based overlay) + +**Sticky NavBar (top of content area):** +- Lives inside `SidebarInset`, above the `` +- Sticky header with `sticky top-0 z-30 backdrop-blur` styling (same pattern as current `NavBar.tsx`) +- Left side: `SidebarTrigger` (hamburger to toggle sidebar on mobile, collapse on desktop) +- Left side (optional): `Separator` + `Breadcrumb` for page context +- Right side: account `DropdownMenu` with theme picker submenu, language submenu, and logout — same structure as current `NavBar.tsx` +- The hamburger no longer needs `drawerOpenAtom` — `SidebarTrigger` handles toggle internally + +**Structure in code:** +```tsx + + + {/* nav items */} + {/* theme + lang switches */} + + +
+ +
+ {/* account dropdown */} +
+
+ +
+
+
+``` + +### Jotai cleanup + +- Remove modal-related atoms +- Remove `drawerOpenAtom` from `atoms/layout.ts` (sidebar trigger handles toggle internally now) +- Auth atoms, theme atoms, and other app state atoms remain untouched + +### Provider stack in `main.tsx` + +- Add `` (Sonner) +- Remove any modal-manager provider +- Keep `ApolloProvider` → `ThemeProvider` → `TooltipProvider` → `HelmetProvider` → `Suspense` order + +## Section 3: Page Migration Waves + +### Wave 1 — Auth pages (smoke test) + +- `Login.tsx` — Update form to use new `Input`, `Button`, `Label` +- `CreateAccount.tsx` — Same treatment +- `EmailPasswordForm.tsx` — Shared form component, update imports + +### Wave 2 — Server pages (most complex) + +- `ServerList.tsx` / `ServerRow.tsx` / `ServerCard.tsx` — Update `Card`, `Badge`, `Button`, `DropdownMenu` variants +- `ServerContainer.tsx` — Parent layout for server detail area +- `ServerStat.tsx` / `ServerPortsStat.tsx` / `ServerTrafficStat.tsx` / `ServerSSHStat.tsx` — Stat cards +- `ServerInfoModal.tsx` — Convert from ModalManager to inline `Dialog` +- `chart/Chart.tsx` / `chart/Sparkline.tsx` — Mostly untouched unless wrapping in new `chart.tsx` + +### Wave 3 — Port & User pages + +- `ServerPorts.tsx` / `PortCard.tsx` / `PortSelectCard.tsx` / `PortUsersCard.tsx` — Update `Card`, `Select`, `Badge` +- `PortFunctionModal.tsx` / `PortRestrictionModal.tsx` — Convert to inline `Dialog` +- `port/restriction/PortExpiration.tsx` — Form field updates +- `Users.tsx` / `ServerUsers.tsx` — Update `Table`, `Button`, `Badge` + +### Wave 4 — Files & Deployments + +- `FileCenter.tsx` / `FileCenterContainer.tsx` / `FileCard.tsx` / `FileRow.tsx` — Update `Card`, `Table`, `Badge` +- `FileModal.tsx` / `FilePreviewModal.tsx` — Convert to inline `Dialog`/`Sheet` +- `DeploymentList.tsx` / `DeploymentStatusBadge.tsx` — Update `Badge`, `Table` +- `DeployModal.tsx` / `DeploymentDetailModal.tsx` / `BindingModal.tsx` — Convert to inline `Dialog` + +### Wave 5 — Services & Misc + +- `ServiceListPage.tsx` — Update `Card`, `Button`, `Table` +- `ServiceEditorPage.tsx` + panels (`AuthoringJsonPanel`, `FormPreviewPanel`, `CompileOutputPanel`, `ParamEditorPanel`) — Update `Tabs`, `Card`, `Button`, `Input`. `Resizable` from zip could replace custom split-pane. +- Service editor field components — full list: `fields/TextField.tsx`, `fields/SelectField.tsx`, `fields/CheckboxField.tsx`, `fields/TextAreaField.tsx`, `fields/ListField.tsx`, `fields/ObjectField.tsx`, `fields/FieldsRenderer.tsx`, `fields/FieldShell.tsx`, `fields/FieldError.tsx`, `fields/index.ts`. Update `Input`, `Select`, `Checkbox`, `Textarea`, `Label`. +- Additional service editor files: `ParamList.tsx`, `ParamTypeEditor.tsx`, `EmitConfigEditor.tsx`, `ValidationEditor.tsx`, `ConditionEditor.tsx`, `UIConfigEditor.tsx`, `useDynamicForm.tsx`, `serviceAdapter.ts`, `builderUtils.ts`, `formUtils.ts`, `constants.ts`. These use `Input`, `Select`, `Button` and need import updates. +- `About.tsx` / `Themes.tsx` — Simple pages, minimal updates. Themes page may need simplification since we're dropping to zinc light/dark only. +- `Error.tsx` / `NoMatch.tsx` / `Hero.tsx` — Layout utility pages + +### Shared utilities (updated across all waves) + +- `features/ui/PageSection.tsx`, `PageHeader.tsx`, `EmptyState.tsx` — Update to new primitives +- `features/DataLoading.tsx` — Update `Skeleton` or `Spinner` usage +- `features/Paginator.tsx` — Update `Button` variants +- `features/ThemedSuspense.tsx` — Update loading state + +### Modal conversion pattern (applied in every wave) + +1. Remove atom-based open/close logic +2. Add local `useState` or use `Dialog` with `DialogTrigger` +3. Replace `` with `` + `` + `` +4. For destructive confirmations, use `AlertDialog` instead of `Dialog` + +## Section 4: Cleanup & Dependencies + +### Package.json additions + +- `sonner` (toast) +- `vaul` (drawer dependency) +- `embla-carousel-react` (carousel dependency) +- `react-day-picker` + `date-fns` (calendar dependency) +- `input-otp` +- `react-resizable-panels` + +### Package.json removals (after all waves) + +- `@fontsource-variable/noto-sans` +- `@fontsource-variable/space-grotesk` + +### Files to delete after all waves + +- `features/modal/ModalManager.tsx` +- `features/modal/ConfirmationModal.tsx` +- `features/ui/ModalShell.tsx` +- `features/Notification.tsx` +- `features/layout/SideBar.tsx` +- `features/layout/NavBar.tsx` +- `atoms/notification.ts` +- `atoms/notificationManager.ts` +- `drawerOpenAtom` from `atoms/layout.ts` +- Modal-related Jotai atoms +- `plugins/tailwind-mix.js` + +### Files to update + +- `App.tsx` — Remove `` and `` renders and their lazy imports +- `graphql.ts` — Replace `notify()` calls with Sonner `toast()` calls +- `features/server/ServerRow.tsx` — Replace `addNotification()` with `toast()` +- `features/auth/EmailPasswordForm.tsx` — Replace `addNotification()` with `toast()` + +### Files to keep but review + +- `features/theme/ThemeSwitch.tsx` — Adapt for sidebar footer; simplify to light/dark toggle only +- `features/i18n/LanguageSwitch.tsx` — Adapt for sidebar footer +- `features/ThemedSuspense.tsx` — May simplify with `Spinner` or `Skeleton` +- `components/theme-provider.tsx` — Keep as-is; infrastructure works for zinc light/dark and future themes + +### Untouched + +- All GraphQL queries (`src/queries/`) +- All Jotai atoms except modal/notification-related (`src/atoms/`) +- All existing hooks (`src/hooks/`) except new additions +- `routes.ts` +- `graphql.ts` — Only change is replacing `notify()` calls with Sonner `toast()` calls +- `tailwind-safelist.ts` — Still needed for service editor dynamic grid layouts +- Backend — no changes + +## Appendix: Known Transition Period + +Between Section 2 (layout rebuild) and Wave 1 completion, the entire app will be visually broken — the old layout shell is removed but pages haven't been updated yet. This is acceptable for a single-owner project. Each wave restores functionality to its page group. diff --git a/docs/superpowers/specs/2026-03-24-shadcn-migration-design.md b/docs/superpowers/specs/2026-03-24-shadcn-migration-design.md new file mode 100644 index 0000000..38ac0fa --- /dev/null +++ b/docs/superpowers/specs/2026-03-24-shadcn-migration-design.md @@ -0,0 +1,292 @@ +# Frontend Migration: DaisyUI to shadcn/ui + TypeScript + +**Date:** 2026-03-24 +**Branch:** `feature/migrate-frontend-to-shadcn` +**Status:** Approved + +## Overview + +Full migration of the Aurora frontend from DaisyUI 5 + JavaScript to shadcn/ui + TypeScript. Includes React 18 to 19 upgrade, Redux removal (consolidate to Jotai + Apollo), and service editor refactor. + +## Decisions + +| Decision | Choice | Rationale | +|---|---|---| +| Migration strategy | Build shadcn component library first, then incremental in-place feature migration (bulk swap only if DaisyUI config must break) | Keeps app working throughout, components designed holistically | +| Language | TypeScript (strict) | Files converted per-feature during migration | +| React version | 19.x | Aligned with shadcn-template | +| State management | Jotai only (remove Redux) | Eliminate dual-system complexity | +| Theming | Multi-theme via CSS classes + shadcn variables | 4 initial themes, extensible | +| Font | Noto Sans Variable | From shadcn-template | +| `tailwind-mix.js` plugin | Keep available, but prefer shadcn CSS variables for new code | No rework needed, backward compatible | +| Service editor | Refactor during migration (break up 1114-line monolith) | Overdue, natural opportunity | +| Framer Motion | Keep alongside `tw-animate-css` | Different purposes: Framer for orchestrated transitions, tw-animate for CSS utility animations | +| `react-hook-form` | Keep as-is | Already has good TS support, used by service editor's `useDynamicForm` | +| Vite React plugin | Keep `@vitejs/plugin-react-swc` (SWC) | Already in use, faster builds, SWC supports React 19 | + +## Section 1: Foundation Setup + +### TypeScript + +- Add `tsconfig.json` with strict mode, `@/*` path aliases (based on shadcn-template config) +- Add `tsconfig.app.json` (ES2022 target, ESNext module) and `tsconfig.node.json` (ES2023, for vite config) +- Add `vite-env.d.ts` and type declarations for non-TS dependencies +- Rename entry points: `main.jsx` -> `main.tsx`, `App.jsx` -> `App.tsx` +- Remaining files converted from `.jsx` -> `.tsx` as each feature is migrated + +### React 19 + +- Bump `react` and `react-dom` from 18.2 to 19.x +- Replace `react-helmet` with `react-helmet-async` (react-helmet uses legacy string refs, incompatible with React 19) +- Audit `react-loading` for React 19 compatibility; replace if needed +- Audit remaining deps for deprecated patterns (legacy context, string refs) + +### Tailwind/CSS Coexistence Layer + +DaisyUI and shadcn CSS variables can coexist during migration while DaisyUI remains in the tree: +- DaisyUI uses `--color-base-100`, `--color-primary`, etc. with `data-theme` attribute +- shadcn uses `--background`, `--foreground`, `--primary`, etc. with CSS classes + +If token conflicts make coexistence awkward, it is acceptable to let DaisyUI styling degrade or break during the migration window rather than adding complex compatibility shims. DaisyUI is temporary and will be removed in Phase 5. + +Changes to `index.css`: +- Keep DaisyUI plugin active during migration +- Add shadcn imports: `shadcn/tailwind.css`, `tw-animate-css` +- Add shadcn `@theme inline` block with CSS variable mappings +- Add theme class definitions (see Section 2 for details) +- Add `@fontsource-variable/noto-sans` import +- Preserve custom breakpoints (`ssm: 320px`, `xs: 475px`) in shadcn `@theme` block (used by `ServerPorts`, `ServerUsers`) + +New files: +- `src/lib/utils.ts` — `cn()` utility (clsx + tailwind-merge) +- `components.json` — shadcn CLI configuration + +### Package Changes + +**Add:** +- `radix-ui` — headless UI primitives +- `class-variance-authority` — component variant system +- `clsx`, `tailwind-merge` — class composition +- `shadcn` — CLI for adding components +- `tw-animate-css` — animation utilities +- `@fontsource-variable/noto-sans` — font +- `typescript`, `@types/react`, `@types/react-dom` — TypeScript tooling +- `react-helmet-async` — React 19-compatible helmet + +**Remove (Phase 5, after full migration):** +- `daisyui` +- `@reduxjs/toolkit`, `redux-persist`, `react-redux` +- `classnames` (replaced by `cn()` / `clsx`) +- `react-helmet` (replaced by `react-helmet-async`) + +### `classnames` to `cn()` Migration + +The `classnames` package is imported in ~30 files. Replace with `cn()` (clsx + tailwind-merge) as each feature is migrated: +- When converting a file from JSX to TSX, replace `import classnames from 'classnames'` with `import { cn } from '@/lib/utils'` +- `cn()` is a drop-in replacement for `classnames()` with the added benefit of Tailwind class deduplication +- Remove `classnames` package in Phase 5 after all files are converted + +## Section 2: Theme System + +### Architecture + +Each theme is a named CSS class (e.g., `.theme-aurora-classic`, `.theme-sunset`) applied to ``. Each class sets all shadcn CSS variables using OKLCH values. + +**Dark mode variant:** The shadcn-template's `@custom-variant dark (&:is(.dark *))` must be reworked. Instead of a single `.dark` class, dark themes apply both their named class AND `.dark`: +```html + + + + +``` +This ensures shadcn's `dark:` variant works correctly for all dark themes. The theme provider determines `color-scheme` per theme (light or dark) and applies `.dark` accordingly. + +**CSS structure:** +```css +/* Default/fallback (aurora-classic) */ +:root { --background: oklch(...); --foreground: oklch(...); ... } + +/* Each theme overrides all variables */ +.theme-aurora-classic { --background: ...; --primary: ...; ... } +.theme-sunset { --background: ...; --primary: ...; ... } +.theme-morning { --background: ...; --primary: ...; ... } +.theme-midnight { --background: ...; --primary: ...; ... } +``` + +### Initial Themes (4) + +| Theme | Type | Primary | Base | +|---|---|---|---| +| `aurora-classic` | light | Purple (#7E3AF2) | White | +| `sunset` | dark | Coral (#EE8679) | Dark navy | +| `morning` | light | Terracotta (#D26A5D) | Light | +| `midnight` | dark | Purple | Dark (new) | + +Each theme defines the full shadcn variable set: `--background`, `--foreground`, `--primary`, `--primary-foreground`, `--secondary`, `--secondary-foreground`, `--muted`, `--muted-foreground`, `--accent`, `--accent-foreground`, `--destructive`, `--border`, `--input`, `--ring`, `--card`, `--card-foreground`, `--popover`, `--popover-foreground`, `--sidebar-*`, `--chart-*`, `--radius`. + +### Theme Provider + +- Custom `ThemeProvider` (rewritten from shadcn-template to support named themes, not just light/dark toggle) +- Keep Jotai `themeSelectionAtom` for localStorage persistence +- "Auto" mode: `prefers-color-scheme` -> aurora-classic (light) or sunset (dark) +- Apply theme class + `.dark` (for dark themes) to `` element +- Keyboard shortcut `d` for quick light/dark toggle (cycles between last-used light and dark themes) + +### DaisyUI Theme Reference + +Existing 3 custom DaisyUI theme definitions preserved in `src/styles/daisyui-themes-reference.css` (not imported, reference only). + +### Theme Switcher UI + +Migrate `ThemeSwitch` / `ThemeMenuItems` to TSX with shadcn `DropdownMenu`. Shows 4 themes + "Auto" option. + +## Section 3: shadcn Component Library + +### Components to Build + +| shadcn Component | Replaces | Notes | +|---|---|---| +| `Button` | `btn btn-primary btn-ghost btn-sm` | Extend with Aurora sizes | +| `Input` | `input input-bordered input-error` | With error state styling | +| `Textarea` | `textarea textarea-bordered` | | +| `Select` | `select select-bordered` | Radix Select | +| `Checkbox` | `checkbox` | | +| `Toggle` | `toggle` | Radix Switch | +| `Badge` | `badge badge-primary` | | +| `Card` | Custom `rounded-xl border` cards | Standardize pattern | +| `Dialog` | `modal modal-box modal-backdrop` | Radix Dialog, replaces ModalShell | +| `Sheet` | `drawer drawer-side` | Radix Sheet for mobile sidebar | +| `DropdownMenu` | `dropdown` + `
` wrapper | Radix DropdownMenu | +| `Tabs` | `tabs tab tab-active` | | +| `Alert` | `alert alert-info alert-success` | | +| `Progress` | `progress` | | +| `Tooltip` | (new) | | +| `Label` | (new) | Form labels | +| `Separator` | (new) | Replaces `divider` | +| `ScrollArea` | (new) | Scrollable panels | +| `Skeleton` | (new) | Loading states | +| `Table` | Raw `` with Tailwind | Standardized | + +**Not needed / deferred:** NavigationMenu, Accordion, Calendar/DatePicker, Command/Combobox. + +### Component Location + +`src/components/ui/` — matches shadcn convention and `components.json` aliases. + +### Approach + +Use `npx shadcn@latest add ` for the base, then customize styling to match Aurora's design language. Components needing significant customization (e.g., Dialog integrating with modal atom system) are built on top of the shadcn base. + +### Custom Layout Components + +- `AppShell` — replaces DaisyUI drawer layout (sidebar + content area) +- `NavBar` — migrated from DaisyUI navbar to Tailwind + shadcn primitives +- `SideBar` — migrated from DaisyUI menu to custom shadcn-styled sidebar + +## Section 4: Feature Migration Order + +Incremental migration. Each feature converts JSX -> TSX and DaisyUI -> shadcn. If DaisyUI config must break at any point, remaining features are bulk-swapped. + +### Phase 1 — Core Shell + Shared Infrastructure + +1. Convert core atoms to TypeScript: `atoms/auth.ts`, `atoms/modal.ts`, `atoms/theme.ts`, `atoms/notification.ts`, `atoms/layout.ts` (consumed by nearly every feature, must be done first) +2. Migrate notification system from Redux to Jotai notification atom (cross-cutting concern, blocks clean migration of all features that do error handling) +3. Rework GraphQL codegen for TypeScript/Apollo migration: + - Point schema to `http://aurora.localhost:8060/api/graphql` (or equivalent local backend endpoint when needed) + - Update document globs to include `.ts` and `.tsx`, not only `.jsx` + - Move generated base types from `store/apis/types.generated.ts` to `src/types/generated.ts` + - Remove RTK Query-oriented generation from the migration path; missing GraphQL coverage is documented as TODO, not a blocker +4. `Layout.tsx` + `features/layout/NavBar.tsx` + `features/layout/SideBar.tsx` — the app skeleton +5. `features/modal/ModalManager.tsx` — rewrite with shadcn Dialog, keep Jotai modal atom stack +6. `features/theme/` — new theme switcher with shadcn DropdownMenu +7. `features/i18n/` — language switcher +8. `features/Notification.tsx` — shared notification component +9. `features/Paginator.tsx` — shared pagination component + +### Phase 2 — Auth + Simple Features + +10. `features/auth/` — login, create account (Input, Button, Card) +11. `features/about/` — static page +12. `features/user/` — user management (Table, Badge, Dialog) + +### Phase 3 — Core Business Features + +13. `features/server/` — server list, cards, stats +14. `features/port/` — port management modals +15. `features/file/` — file management + +### Phase 4 — Complex Features + +16. `features/deployment/` — DeployModal, DeploymentList, BindingModal +17. `features/service-editor/` — refactor + migrate (see Section 6) + +### Phase 5 — Cleanup + +18. Remove DaisyUI plugin + package +19. Remove Redux + redux-persist + react-redux +20. Remove legacy `store/` directory +21. Remove `classnames` package +22. Remove `react-helmet` package +23. Clean up remaining `.jsx` files, dead imports, unused CSS +24. Review `tailwind-safelist.js` — remove if dynamic class safelist is no longer needed, or convert to TS +25. Remove DaisyUI theme reference if no longer needed + +### Shared Infrastructure + +Converted as each consuming feature is migrated: +- `hooks/` — convert to `.ts`/`.tsx` +- `queries/` — add types +- `graphql.js` -> `graphql.ts` +- `routes.js` -> `routes.ts` +- `i18n.js` -> `i18n.ts` + +### `src/apis/` Directory + +The `src/apis/` directory (5 files: auth, ports, servers, users, utils) uses `axios` for REST calls. `apis/auth.js` is used by the Jotai auth reducer for token validation. These are kept and converted to TypeScript during migration. Whether to replace them with Apollo/GraphQL calls is out of scope — document as a future TODO if applicable. + +## Section 5: Redux Removal + +### Strategy + +Per-feature during Phases 2-4: +1. Audit what the feature pulls from Redux +2. For RTK Query endpoints: confirm Apollo covers the same data, remove RTK Query usage +3. For persisted Redux state: migrate to Jotai `atomWithStorage` +4. If a feature relies on Redux for data not yet available via GraphQL: document what did not migrate cleanly as a TODO for later GraphQL implementation (do not block migration) + +### Cross-Cutting Concerns (Phase 1) + +- **Notification system:** `showNotification` Redux thunk is used across features for error handling. Migrate to Jotai `notificationManager` atom early in Phase 1 to unblock all subsequent feature migrations. +- **WebSocket manager:** `store/websocketManager.ts` and related Redux websocket plumbing are legacy and should be removed. The real app uses GraphQL subscriptions via Apollo, so websocket manager removal is part of the Redux cleanup, not a separate investigation. +- **Generated types:** `store/apis/types.generated.ts` exports `FileTypeEnum` used by `FileModal` and `ServerInfoModal`. Move to `src/types/generated.ts` in Phase 1. + +### Final Cleanup (Phase 5) + +- Remove `Provider` + `PersistGate` wrappers from `main.tsx` +- Delete `store/` directory +- Remove packages: `@reduxjs/toolkit`, `redux-persist`, `react-redux` + +## Section 6: Service Editor Refactor + +### ParamEditorPanel Split + +The 1114-line `ParamEditorPanel.jsx` is broken into focused modules: + +| New File | Responsibility | +|---|---| +| `ParamEditorPanel.tsx` | Shell — renders selected param's editor, manages selection state | +| `ParamList.tsx` | Left sidebar list of params with add/remove/reorder | +| `EmitConfigEditor.tsx` | Emit target configuration (arg, flag, env, file, stdin, pos) | +| `ValidationEditor.tsx` | Validation rules (min, max, pattern, etc.) | +| `ConditionEditor.tsx` | Conditional visibility rules | +| `UIConfigEditor.tsx` | UI hints (grid, placeholder, description) | +| `ParamTypeEditor.tsx` | Type selection + type-specific options | + +Each module receives the current param draft and an `onChange` callback. The shell orchestrates them in tabs or sections. + +### Field Components Migration + +- Swap DaisyUI form classes for shadcn `Input`, `Select`, `Checkbox`, `Textarea`, `Label` +- `FieldShell` becomes a wrapper using shadcn `Label` + error display +- `ListField` and `ObjectField` keep recursive rendering logic, restyled with shadcn components +- `react-hook-form` integration stays as-is (already well-typed) diff --git a/frontend b/frontend index 7c8bcde..65d7fa8 160000 --- a/frontend +++ b/frontend @@ -1 +1 @@ -Subproject commit 7c8bcde889dda8a7e86e41497f64525471da3ee1 +Subproject commit 65d7fa8117230832a28e1166ee8180f5e2646eb3 diff --git a/frontend-old b/frontend-old index ff28aa2..9585ad8 160000 --- a/frontend-old +++ b/frontend-old @@ -1 +1 @@ -Subproject commit ff28aa29df9e1d55d33804e88767d565edf8c70e +Subproject commit 9585ad8b1f8a8ce42fdb98be0953b10477f54d12 diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 43f0a8f..c1ef793 100755 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -18,7 +18,7 @@ server { } location / { - proxy_pass http://frontend-old:3000; + proxy_pass http://frontend:5173; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr;