From 1c669327d078aecad5c77694a65075776a825462 Mon Sep 17 00:00:00 2001 From: Bury Huang Date: Wed, 1 Apr 2026 22:15:02 -0700 Subject: [PATCH 1/3] fix: workspace migration bugs + add ECS deployment support Fix migration issues that block workspace backend on real PostgreSQL: - Migration 005: fix UUID type mismatch on workspace_collaborators FK - Migration 006: add missing browser_tabs and browser_usage tables - Migration 007: add missing files table and columns on channels/members - Dockerfile: set PYTHONPATH so alembic can find app module - cli_agent.py: fix missing imports (Progress, yaml, configure_workspace_logging) Add ECS Fargate deployment template and documentation. --- deploy/ecs/README.md | 91 +++++++++++++++++++ deploy/ecs/task-definition.json | 36 ++++++++ src/openagents/client/cli_agent.py | 3 + workspace/README.md | 56 ++++++++++++ workspace/backend/Dockerfile | 4 + .../005_add_workspace_collaborators.py | 5 +- .../versions/006_add_browser_contexts.py | 46 ++++++++-- .../007_catchup_missing_columns_and_tables.py | 49 ++++++++++ workspace/backend/entrypoint.sh | 2 + 9 files changed, 284 insertions(+), 8 deletions(-) create mode 100644 deploy/ecs/README.md create mode 100644 deploy/ecs/task-definition.json create mode 100644 workspace/backend/alembic/versions/007_catchup_missing_columns_and_tables.py diff --git a/deploy/ecs/README.md b/deploy/ecs/README.md new file mode 100644 index 000000000..8960d2e9c --- /dev/null +++ b/deploy/ecs/README.md @@ -0,0 +1,91 @@ +# Deploy Workspace Backend to AWS ECS Fargate + +## Prerequisites + +- AWS CLI configured +- Docker installed +- An ECR repository for the image +- A PostgreSQL database (e.g., Supabase, Insforge, RDS) + +## 1. Create ECR Repository + +```bash +aws ecr create-repository --repository-name openagents-workspace-backend --region us-east-1 +``` + +## 2. Build and Push Image + +```bash +cd workspace/backend + +aws ecr get-login-password --region us-east-1 | \ + docker login --username AWS --password-stdin .dkr.ecr.us-east-1.amazonaws.com + +docker build --platform linux/amd64 -t .dkr.ecr.us-east-1.amazonaws.com/openagents-workspace-backend:latest . +docker push .dkr.ecr.us-east-1.amazonaws.com/openagents-workspace-backend:latest +``` + +## 3. Create ECS Resources + +```bash +# Create cluster +aws ecs create-cluster --cluster-name openagents-workspace + +# Create log group +aws logs create-log-group --log-group-name /ecs/openagents-workspace-backend + +# Edit task-definition.json with your values, then register it +aws ecs register-task-definition --cli-input-json file://deploy/ecs/task-definition.json +``` + +## 4. Create Service + +```bash +# Create a security group allowing port 8000 (or 443 if using ALB) +SG_ID=$(aws ec2 create-security-group \ + --group-name openagents-workspace-sg \ + --description "OpenAgents Workspace" \ + --vpc-id \ + --query 'GroupId' --output text) + +aws ec2 authorize-security-group-ingress \ + --group-id $SG_ID \ + --protocol tcp --port 8000 --cidr 0.0.0.0/0 + +# Create Fargate service +aws ecs create-service \ + --cluster openagents-workspace \ + --service-name workspace-backend \ + --task-definition openagents-workspace-backend \ + --desired-count 1 \ + --launch-type FARGATE \ + --network-configuration "awsvpcConfiguration={subnets=[,],securityGroups=[$SG_ID],assignPublicIp=ENABLED}" +``` + +## 5. Add HTTPS with ALB (Recommended) + +For a stable URL with SSL: + +1. Request an ACM certificate for your domain +2. Add DNS validation CNAME to Route 53 +3. Create an ALB with HTTPS listener forwarding to a target group on port 8000 +4. Update ECS service with the load balancer target group +5. Add a Route 53 A record (alias) pointing your domain to the ALB + +## 6. Update Image + +```bash +docker build --platform linux/amd64 -t .dkr.ecr.us-east-1.amazonaws.com/openagents-workspace-backend:latest . +docker push .dkr.ecr.us-east-1.amazonaws.com/openagents-workspace-backend:latest +aws ecs update-service --cluster openagents-workspace --service workspace-backend --force-new-deployment +``` + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `DATABASE_URL` | Yes | PostgreSQL connection string | +| `AUTH_MODE` | No | `workspace_token` (default) or `firebase` | +| `IDENTITY_MODE` | No | `standalone` (default) or `shared` | +| `CORS_ORIGINS` | No | Allowed origins, default `*` | +| `WORKSPACE_ENDPOINT` | No | Public URL of this service (used in manifests) | diff --git a/deploy/ecs/task-definition.json b/deploy/ecs/task-definition.json new file mode 100644 index 000000000..c4503a97a --- /dev/null +++ b/deploy/ecs/task-definition.json @@ -0,0 +1,36 @@ +{ + "family": "openagents-workspace-backend", + "networkMode": "awsvpc", + "requiresCompatibilities": ["FARGATE"], + "cpu": "512", + "memory": "1024", + "executionRoleArn": "arn:aws:iam:::role/ecsTaskExecutionRole", + "containerDefinitions": [ + { + "name": "workspace-backend", + "image": ".dkr.ecr..amazonaws.com/openagents-workspace-backend:latest", + "portMappings": [ + { + "containerPort": 8000, + "protocol": "tcp" + } + ], + "environment": [ + { "name": "DATABASE_URL", "value": "postgresql://user:pass@host:5432/dbname?sslmode=require" }, + { "name": "AUTH_MODE", "value": "workspace_token" }, + { "name": "IDENTITY_MODE", "value": "standalone" }, + { "name": "CORS_ORIGINS", "value": "*" }, + { "name": "WORKSPACE_ENDPOINT", "value": "https://your-domain.example.com" } + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/ecs/openagents-workspace-backend", + "awslogs-region": "", + "awslogs-stream-prefix": "ecs" + } + }, + "essential": true + } + ] +} diff --git a/src/openagents/client/cli_agent.py b/src/openagents/client/cli_agent.py index 8983a10f7..1ede2354a 100644 --- a/src/openagents/client/cli_agent.py +++ b/src/openagents/client/cli_agent.py @@ -6,10 +6,13 @@ from typing import Optional import typer +import yaml from rich.panel import Panel +from rich.progress import Progress, SpinnerColumn, TextColumn from rich.table import Table from rich import box +from openagents.client.cli_helpers import configure_workspace_logging from openagents.client.cli_shared import app, console agent_app = typer.Typer( diff --git a/workspace/README.md b/workspace/README.md index 5bc55f9d8..9a2506919 100644 --- a/workspace/README.md +++ b/workspace/README.md @@ -40,6 +40,62 @@ Events flow through a mod pipeline: `mod/auth` → `mod/workspace` → `mod/pers | `CORS_ORIGINS` | `*` | Allowed CORS origins (comma-separated) | | `AGENT_TIMEOUT_SECONDS` | `60` | Seconds before agent is considered offline | +## Self-Hosting + +### Run Locally (with external PostgreSQL) + +```bash +cd workspace/backend +pip install -r requirements.txt + +DATABASE_URL="postgresql://user:pass@host:5432/dbname?sslmode=require" \ +AUTH_MODE=workspace_token \ +PYTHONPATH=. \ +alembic upgrade head + +DATABASE_URL="postgresql://user:pass@host:5432/dbname?sslmode=require" \ +AUTH_MODE=workspace_token \ +PYTHONPATH=. \ +uvicorn app.main:app --host 0.0.0.0 --port 8000 +``` + +### Deploy to AWS ECS + +Build and push the Docker image: + +```bash +cd workspace/backend +aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin .dkr.ecr.us-east-1.amazonaws.com +docker build --platform linux/amd64 -t .dkr.ecr.us-east-1.amazonaws.com/openagents-workspace-backend:latest . +docker push .dkr.ecr.us-east-1.amazonaws.com/openagents-workspace-backend:latest +``` + +See [`deploy/ecs/task-definition.json`](../deploy/ecs/task-definition.json) for the Fargate task definition template. + +### Connect Agents + +```bash +# Create a workspace +curl -X POST https://your-endpoint/v1/workspaces \ + -H "Content-Type: application/json" \ + -d '{"name": "my-workspace"}' +# Returns: { "data": { "token": "", "slug": "" } } + +# Connect an agent +openagents create claude --name my-agent \ + --join-workspace \ + --endpoint https://your-endpoint \ + --no-browser +``` + +### Run Frontend + +```bash +cd workspace/frontend +npm install +NEXT_PUBLIC_API_URL=https://your-endpoint npm run dev +``` + ## Development ```bash diff --git a/workspace/backend/Dockerfile b/workspace/backend/Dockerfile index 384ab8304..27ef6c1f3 100644 --- a/workspace/backend/Dockerfile +++ b/workspace/backend/Dockerfile @@ -2,6 +2,8 @@ FROM python:3.12-slim WORKDIR /app +ENV PYTHONPATH=/app + # Install dependencies COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt @@ -9,5 +11,7 @@ RUN pip install --no-cache-dir -r requirements.txt # Copy application code COPY . . +RUN chmod +x entrypoint.sh + EXPOSE 8000 ENTRYPOINT ["./entrypoint.sh"] diff --git a/workspace/backend/alembic/versions/005_add_workspace_collaborators.py b/workspace/backend/alembic/versions/005_add_workspace_collaborators.py index 8d1b1784d..89d89cd6a 100644 --- a/workspace/backend/alembic/versions/005_add_workspace_collaborators.py +++ b/workspace/backend/alembic/versions/005_add_workspace_collaborators.py @@ -8,6 +8,7 @@ from alembic import op import sqlalchemy as sa +from sqlalchemy.dialects import postgresql revision = "005" down_revision = "004" @@ -18,8 +19,8 @@ def upgrade() -> None: op.create_table( "workspace_collaborators", - sa.Column("id", sa.Text(), primary_key=True, server_default=sa.text("gen_random_uuid()")), - sa.Column("workspace_id", sa.Text(), sa.ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False), + sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")), + sa.Column("workspace_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False), sa.Column("email", sa.Text(), nullable=False), sa.Column("role", sa.Text(), server_default="editor"), sa.Column("added_by", sa.Text(), nullable=True), diff --git a/workspace/backend/alembic/versions/006_add_browser_contexts.py b/workspace/backend/alembic/versions/006_add_browser_contexts.py index 7e3ed2df8..a7025972d 100644 --- a/workspace/backend/alembic/versions/006_add_browser_contexts.py +++ b/workspace/backend/alembic/versions/006_add_browser_contexts.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Add browser_contexts table and context_id FK on browser_tabs. +"""Add browser_tabs, browser_usage, browser_contexts tables. Revision ID: 006 Revises: 005 @@ -8,6 +8,7 @@ from alembic import op import sqlalchemy as sa +from sqlalchemy.dialects import postgresql revision = "006" down_revision = "005" @@ -16,11 +17,11 @@ def upgrade() -> None: - # Create browser_contexts table + # Create browser_contexts table first (browser_tabs has FK to it) op.create_table( "browser_contexts", sa.Column("id", sa.Text(), primary_key=True, server_default=sa.text("gen_random_uuid()")), - sa.Column("workspace_id", sa.dialects.postgresql.UUID(as_uuid=False), sa.ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False), + sa.Column("workspace_id", postgresql.UUID(as_uuid=False), sa.ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False), sa.Column("name", sa.Text(), nullable=False), sa.Column("bb_context_id", sa.Text(), nullable=True), sa.Column("domain", sa.Text(), nullable=True), @@ -33,14 +34,47 @@ def upgrade() -> None: ) op.create_index("idx_browser_contexts_workspace_status", "browser_contexts", ["workspace_id", "status"]) - # Add context_id FK to browser_tabs - op.add_column( + # Create browser_tabs table (was missing from migrations, only auto-created in SQLite dev mode) + op.create_table( "browser_tabs", + sa.Column("id", sa.Text(), primary_key=True, server_default=sa.text("gen_random_uuid()")), + sa.Column("workspace_id", postgresql.UUID(as_uuid=False), sa.ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False), + sa.Column("url", sa.Text(), nullable=False, server_default="about:blank"), + sa.Column("title", sa.Text(), nullable=True), + sa.Column("status", sa.Text(), nullable=False, server_default="active"), + sa.Column("created_by", sa.Text(), nullable=False), + sa.Column("shared_with", postgresql.JSONB(), server_default="[]"), sa.Column("context_id", sa.Text(), sa.ForeignKey("browser_contexts.id", ondelete="SET NULL"), nullable=True), + sa.Column("session_id", sa.Text(), nullable=True), + sa.Column("live_url", sa.Text(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()")), + sa.Column("last_active_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()")), + ) + op.create_index("idx_browser_tabs_workspace_status", "browser_tabs", ["workspace_id", "status"]) + + # Create browser_usage table (also missing from migrations) + op.create_table( + "browser_usage", + sa.Column("id", sa.Text(), primary_key=True, server_default=sa.text("gen_random_uuid()")), + sa.Column("workspace_id", postgresql.UUID(as_uuid=False), sa.ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False), + sa.Column("tab_id", sa.Text(), nullable=False), + sa.Column("session_id", sa.Text(), nullable=True), + sa.Column("opened_by", sa.Text(), nullable=False), + sa.Column("started_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("NOW()")), + sa.Column("ended_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("duration_seconds", sa.Integer(), nullable=True), ) + op.create_index("idx_browser_usage_workspace", "browser_usage", ["workspace_id"]) + op.create_index("idx_browser_usage_opened_by", "browser_usage", ["opened_by"]) + op.create_index("idx_browser_usage_started", "browser_usage", ["started_at"]) def downgrade() -> None: - op.drop_column("browser_tabs", "context_id") + op.drop_index("idx_browser_usage_started", "browser_usage") + op.drop_index("idx_browser_usage_opened_by", "browser_usage") + op.drop_index("idx_browser_usage_workspace", "browser_usage") + op.drop_table("browser_usage") + op.drop_index("idx_browser_tabs_workspace_status", "browser_tabs") + op.drop_table("browser_tabs") op.drop_index("idx_browser_contexts_workspace_status", "browser_contexts") op.drop_table("browser_contexts") diff --git a/workspace/backend/alembic/versions/007_catchup_missing_columns_and_tables.py b/workspace/backend/alembic/versions/007_catchup_missing_columns_and_tables.py new file mode 100644 index 000000000..83258d5fa --- /dev/null +++ b/workspace/backend/alembic/versions/007_catchup_missing_columns_and_tables.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +"""Catch-up: add files table and missing columns on channels/workspace_members. + +Revision ID: 007 +Revises: 006 +Create Date: 2026-04-02 +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +revision = "007" +down_revision = "006" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Files table (missing from initial migration) + op.create_table( + "files", + sa.Column("id", sa.Text(), primary_key=True, server_default=sa.text("gen_random_uuid()")), + sa.Column("workspace_id", postgresql.UUID(as_uuid=False), sa.ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False), + sa.Column("filename", sa.Text(), nullable=False), + sa.Column("content_type", sa.Text(), nullable=False, server_default="application/octet-stream"), + sa.Column("size", sa.Integer(), nullable=False), + sa.Column("storage_key", sa.Text(), nullable=False), + sa.Column("uploaded_by", sa.Text(), nullable=False), + sa.Column("channel_name", sa.Text(), nullable=True), + sa.Column("status", sa.Text(), nullable=False, server_default="active"), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()")), + ) + op.create_index("idx_files_workspace_status", "files", ["workspace_id", "status"]) + + # Missing columns on channels + op.add_column("channels", sa.Column("title_manually_set", sa.Boolean(), server_default=sa.text("FALSE"))) + op.add_column("channels", sa.Column("resume_from", sa.Text(), nullable=True)) + + # Missing column on workspace_members + op.add_column("workspace_members", sa.Column("description", sa.Text(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("workspace_members", "description") + op.drop_column("channels", "resume_from") + op.drop_column("channels", "title_manually_set") + op.drop_index("idx_files_workspace_status", "files") + op.drop_table("files") diff --git a/workspace/backend/entrypoint.sh b/workspace/backend/entrypoint.sh index baee98c4f..cdfd44110 100755 --- a/workspace/backend/entrypoint.sh +++ b/workspace/backend/entrypoint.sh @@ -1,6 +1,8 @@ #!/bin/sh set -e +export PYTHONPATH=/app + # Run database migrations alembic upgrade head From 7eec95c9911fc8c35e642ef3164b616a729faeb3 Mon Sep 17 00:00:00 2001 From: Bury Huang Date: Wed, 1 Apr 2026 22:22:19 -0700 Subject: [PATCH 2/3] chore: drop unrelated cli_agent.py import fix from PR scope --- src/openagents/client/cli_agent.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/openagents/client/cli_agent.py b/src/openagents/client/cli_agent.py index 1ede2354a..8983a10f7 100644 --- a/src/openagents/client/cli_agent.py +++ b/src/openagents/client/cli_agent.py @@ -6,13 +6,10 @@ from typing import Optional import typer -import yaml from rich.panel import Panel -from rich.progress import Progress, SpinnerColumn, TextColumn from rich.table import Table from rich import box -from openagents.client.cli_helpers import configure_workspace_logging from openagents.client.cli_shared import app, console agent_app = typer.Typer( From 21c37e8b4f0adad36c9d871f49d0145fc8affc04 Mon Sep 17 00:00:00 2001 From: Bury Huang Date: Wed, 1 Apr 2026 22:23:31 -0700 Subject: [PATCH 3/3] =?UTF-8?q?chore:=20remove=20migration=20changes=20?= =?UTF-8?q?=E2=80=94=20upstream=20will=20handle=20schema=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../005_add_workspace_collaborators.py | 5 +- .../versions/006_add_browser_contexts.py | 46 +++-------------- .../007_catchup_missing_columns_and_tables.py | 49 ------------------- 3 files changed, 8 insertions(+), 92 deletions(-) delete mode 100644 workspace/backend/alembic/versions/007_catchup_missing_columns_and_tables.py diff --git a/workspace/backend/alembic/versions/005_add_workspace_collaborators.py b/workspace/backend/alembic/versions/005_add_workspace_collaborators.py index 89d89cd6a..8d1b1784d 100644 --- a/workspace/backend/alembic/versions/005_add_workspace_collaborators.py +++ b/workspace/backend/alembic/versions/005_add_workspace_collaborators.py @@ -8,7 +8,6 @@ from alembic import op import sqlalchemy as sa -from sqlalchemy.dialects import postgresql revision = "005" down_revision = "004" @@ -19,8 +18,8 @@ def upgrade() -> None: op.create_table( "workspace_collaborators", - sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")), - sa.Column("workspace_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False), + sa.Column("id", sa.Text(), primary_key=True, server_default=sa.text("gen_random_uuid()")), + sa.Column("workspace_id", sa.Text(), sa.ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False), sa.Column("email", sa.Text(), nullable=False), sa.Column("role", sa.Text(), server_default="editor"), sa.Column("added_by", sa.Text(), nullable=True), diff --git a/workspace/backend/alembic/versions/006_add_browser_contexts.py b/workspace/backend/alembic/versions/006_add_browser_contexts.py index a7025972d..7e3ed2df8 100644 --- a/workspace/backend/alembic/versions/006_add_browser_contexts.py +++ b/workspace/backend/alembic/versions/006_add_browser_contexts.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Add browser_tabs, browser_usage, browser_contexts tables. +"""Add browser_contexts table and context_id FK on browser_tabs. Revision ID: 006 Revises: 005 @@ -8,7 +8,6 @@ from alembic import op import sqlalchemy as sa -from sqlalchemy.dialects import postgresql revision = "006" down_revision = "005" @@ -17,11 +16,11 @@ def upgrade() -> None: - # Create browser_contexts table first (browser_tabs has FK to it) + # Create browser_contexts table op.create_table( "browser_contexts", sa.Column("id", sa.Text(), primary_key=True, server_default=sa.text("gen_random_uuid()")), - sa.Column("workspace_id", postgresql.UUID(as_uuid=False), sa.ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False), + sa.Column("workspace_id", sa.dialects.postgresql.UUID(as_uuid=False), sa.ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False), sa.Column("name", sa.Text(), nullable=False), sa.Column("bb_context_id", sa.Text(), nullable=True), sa.Column("domain", sa.Text(), nullable=True), @@ -34,47 +33,14 @@ def upgrade() -> None: ) op.create_index("idx_browser_contexts_workspace_status", "browser_contexts", ["workspace_id", "status"]) - # Create browser_tabs table (was missing from migrations, only auto-created in SQLite dev mode) - op.create_table( + # Add context_id FK to browser_tabs + op.add_column( "browser_tabs", - sa.Column("id", sa.Text(), primary_key=True, server_default=sa.text("gen_random_uuid()")), - sa.Column("workspace_id", postgresql.UUID(as_uuid=False), sa.ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False), - sa.Column("url", sa.Text(), nullable=False, server_default="about:blank"), - sa.Column("title", sa.Text(), nullable=True), - sa.Column("status", sa.Text(), nullable=False, server_default="active"), - sa.Column("created_by", sa.Text(), nullable=False), - sa.Column("shared_with", postgresql.JSONB(), server_default="[]"), sa.Column("context_id", sa.Text(), sa.ForeignKey("browser_contexts.id", ondelete="SET NULL"), nullable=True), - sa.Column("session_id", sa.Text(), nullable=True), - sa.Column("live_url", sa.Text(), nullable=True), - sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()")), - sa.Column("last_active_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()")), - ) - op.create_index("idx_browser_tabs_workspace_status", "browser_tabs", ["workspace_id", "status"]) - - # Create browser_usage table (also missing from migrations) - op.create_table( - "browser_usage", - sa.Column("id", sa.Text(), primary_key=True, server_default=sa.text("gen_random_uuid()")), - sa.Column("workspace_id", postgresql.UUID(as_uuid=False), sa.ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False), - sa.Column("tab_id", sa.Text(), nullable=False), - sa.Column("session_id", sa.Text(), nullable=True), - sa.Column("opened_by", sa.Text(), nullable=False), - sa.Column("started_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("NOW()")), - sa.Column("ended_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("duration_seconds", sa.Integer(), nullable=True), ) - op.create_index("idx_browser_usage_workspace", "browser_usage", ["workspace_id"]) - op.create_index("idx_browser_usage_opened_by", "browser_usage", ["opened_by"]) - op.create_index("idx_browser_usage_started", "browser_usage", ["started_at"]) def downgrade() -> None: - op.drop_index("idx_browser_usage_started", "browser_usage") - op.drop_index("idx_browser_usage_opened_by", "browser_usage") - op.drop_index("idx_browser_usage_workspace", "browser_usage") - op.drop_table("browser_usage") - op.drop_index("idx_browser_tabs_workspace_status", "browser_tabs") - op.drop_table("browser_tabs") + op.drop_column("browser_tabs", "context_id") op.drop_index("idx_browser_contexts_workspace_status", "browser_contexts") op.drop_table("browser_contexts") diff --git a/workspace/backend/alembic/versions/007_catchup_missing_columns_and_tables.py b/workspace/backend/alembic/versions/007_catchup_missing_columns_and_tables.py deleted file mode 100644 index 83258d5fa..000000000 --- a/workspace/backend/alembic/versions/007_catchup_missing_columns_and_tables.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -"""Catch-up: add files table and missing columns on channels/workspace_members. - -Revision ID: 007 -Revises: 006 -Create Date: 2026-04-02 -""" - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -revision = "007" -down_revision = "006" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # Files table (missing from initial migration) - op.create_table( - "files", - sa.Column("id", sa.Text(), primary_key=True, server_default=sa.text("gen_random_uuid()")), - sa.Column("workspace_id", postgresql.UUID(as_uuid=False), sa.ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False), - sa.Column("filename", sa.Text(), nullable=False), - sa.Column("content_type", sa.Text(), nullable=False, server_default="application/octet-stream"), - sa.Column("size", sa.Integer(), nullable=False), - sa.Column("storage_key", sa.Text(), nullable=False), - sa.Column("uploaded_by", sa.Text(), nullable=False), - sa.Column("channel_name", sa.Text(), nullable=True), - sa.Column("status", sa.Text(), nullable=False, server_default="active"), - sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()")), - ) - op.create_index("idx_files_workspace_status", "files", ["workspace_id", "status"]) - - # Missing columns on channels - op.add_column("channels", sa.Column("title_manually_set", sa.Boolean(), server_default=sa.text("FALSE"))) - op.add_column("channels", sa.Column("resume_from", sa.Text(), nullable=True)) - - # Missing column on workspace_members - op.add_column("workspace_members", sa.Column("description", sa.Text(), nullable=True)) - - -def downgrade() -> None: - op.drop_column("workspace_members", "description") - op.drop_column("channels", "resume_from") - op.drop_column("channels", "title_manually_set") - op.drop_index("idx_files_workspace_status", "files") - op.drop_table("files")